childprocess 0.9.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 +44 -0
  6. data/CHANGELOG.md +49 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE +20 -0
  9. data/README.md +196 -0
  10. data/Rakefile +61 -0
  11. data/appveyor.yml +60 -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 +422 -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,12 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe ChildProcess::AbstractIO do
4
+ let(:io) { ChildProcess::AbstractIO.new }
5
+
6
+ it "inherits the parent's IO streams" do
7
+ io.inherit!
8
+
9
+ expect(io.stdout).to eq STDOUT
10
+ expect(io.stderr).to eq STDERR
11
+ end
12
+ end
@@ -0,0 +1,422 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../spec_helper', __FILE__)
4
+ require 'rubygems/mock_gem_ui'
5
+
6
+
7
+ describe ChildProcess do
8
+
9
+ here = File.dirname(__FILE__)
10
+
11
+ let(:gemspec) { eval(File.read "#{here}/../childprocess.gemspec") }
12
+
13
+ it 'validates cleanly' do
14
+ mock_ui = Gem::MockGemUi.new
15
+ Gem::DefaultUserInteraction.use_ui(mock_ui) { gemspec.validate }
16
+
17
+ expect(mock_ui.error).to_not match(/warn/i)
18
+ end
19
+
20
+
21
+ it "returns self when started" do
22
+ process = sleeping_ruby
23
+
24
+ expect(process.start).to eq process
25
+ expect(process).to be_alive
26
+ end
27
+
28
+ # We can't detect failure to execve() when using posix_spawn() on Linux
29
+ # without waiting for the child to exit with code 127.
30
+ #
31
+ # See e.g. http://repo.or.cz/w/glibc.git/blob/669704fd:/sysdeps/posix/spawni.c#l34
32
+ #
33
+ # We could work around this by doing the PATH search ourselves, but not sure
34
+ # it's worth it.
35
+ it "raises ChildProcess::LaunchError if the process can't be started", :posix_spawn_on_linux => false do
36
+ expect { invalid_process.start }.to raise_error(ChildProcess::LaunchError)
37
+ end
38
+
39
+ it 'raises ArgumentError if given a non-string argument' do
40
+ expect { ChildProcess.build(nil, "unlikelytoexist") }.to raise_error(ArgumentError)
41
+ expect { ChildProcess.build("foo", 1) }.to raise_error(ArgumentError)
42
+ end
43
+
44
+ it "knows if the process crashed" do
45
+ process = exit_with(1).start
46
+ process.wait
47
+
48
+ expect(process).to be_crashed
49
+ end
50
+
51
+ it "knows if the process didn't crash" do
52
+ process = exit_with(0).start
53
+ process.wait
54
+
55
+ expect(process).to_not be_crashed
56
+ end
57
+
58
+ it "can wait for a process to finish" do
59
+ process = exit_with(0).start
60
+ return_value = process.wait
61
+
62
+ expect(process).to_not be_alive
63
+ expect(return_value).to eq 0
64
+ end
65
+
66
+ it 'ignores #wait if process already finished' do
67
+ process = exit_with(0).start
68
+ sleep 0.01 until process.exited?
69
+
70
+ expect(process.wait).to eql 0
71
+ end
72
+
73
+ it "escalates if TERM is ignored" do
74
+ process = ignored('TERM').start
75
+ process.stop
76
+ expect(process).to be_exited
77
+ end
78
+
79
+ it "accepts a timeout argument to #stop" do
80
+ process = sleeping_ruby.start
81
+ process.stop(exit_timeout)
82
+ end
83
+
84
+ it "lets child process inherit the environment of the current process" do
85
+ Tempfile.open("env-spec") do |file|
86
+ with_env('INHERITED' => 'yes') do
87
+ process = write_env(file.path).start
88
+ process.wait
89
+ end
90
+
91
+ child_env = eval rewind_and_read(file)
92
+ expect(child_env['INHERITED']).to eql 'yes'
93
+ end
94
+ end
95
+
96
+ it "can override env vars only for the current process" do
97
+ Tempfile.open("env-spec") do |file|
98
+ process = write_env(file.path)
99
+ process.environment['CHILD_ONLY'] = '1'
100
+ process.start
101
+
102
+ expect(ENV['CHILD_ONLY']).to be_nil
103
+
104
+ process.wait
105
+
106
+ child_env = eval rewind_and_read(file)
107
+ expect(child_env['CHILD_ONLY']).to eql '1'
108
+ end
109
+ end
110
+
111
+ it "inherits the parent's env vars also when some are overridden" do
112
+ Tempfile.open("env-spec") do |file|
113
+ with_env('INHERITED' => 'yes', 'CHILD_ONLY' => 'no') do
114
+ process = write_env(file.path)
115
+ process.environment['CHILD_ONLY'] = 'yes'
116
+
117
+ process.start
118
+ process.wait
119
+
120
+ child_env = eval rewind_and_read(file)
121
+
122
+ expect(child_env['INHERITED']).to eq 'yes'
123
+ expect(child_env['CHILD_ONLY']).to eq 'yes'
124
+ end
125
+ end
126
+ end
127
+
128
+ it "can unset env vars" do
129
+ Tempfile.open("env-spec") do |file|
130
+ ENV['CHILDPROCESS_UNSET'] = '1'
131
+ process = write_env(file.path)
132
+ process.environment['CHILDPROCESS_UNSET'] = nil
133
+ process.start
134
+
135
+ process.wait
136
+
137
+ child_env = eval rewind_and_read(file)
138
+ expect(child_env).to_not have_key('CHILDPROCESS_UNSET')
139
+ end
140
+ end
141
+
142
+ it 'does not see env vars unset in parent' do
143
+ Tempfile.open('env-spec') do |file|
144
+ ENV['CHILDPROCESS_UNSET'] = nil
145
+ process = write_env(file.path)
146
+ process.start
147
+
148
+ process.wait
149
+
150
+ child_env = eval rewind_and_read(file)
151
+ expect(child_env).to_not have_key('CHILDPROCESS_UNSET')
152
+ end
153
+ end
154
+
155
+
156
+ it "passes arguments to the child" do
157
+ args = ["foo", "bar"]
158
+
159
+ Tempfile.open("argv-spec") do |file|
160
+ process = write_argv(file.path, *args).start
161
+ process.wait
162
+
163
+ expect(rewind_and_read(file)).to eql args.inspect
164
+ end
165
+ end
166
+
167
+ it "lets a detached child live on" do
168
+ p_pid = nil
169
+ c_pid = nil
170
+
171
+ Tempfile.open('grandparent_out') do |gp_file|
172
+ # Create a parent and detached child process that will spit out their PID. Make sure that the child process lasts longer than the parent.
173
+ p_process = ruby("require 'childprocess' ; c_process = ChildProcess.build('ruby', '-e', 'puts \\\"Child PID: \#{Process.pid}\\\" ; sleep 5') ; c_process.io.inherit! ; c_process.detach = true ; c_process.start ; puts \"Child PID: \#{c_process.pid}\" ; puts \"Parent PID: \#{Process.pid}\"")
174
+ p_process.io.stdout = p_process.io.stderr = gp_file
175
+
176
+ # Let the parent process die
177
+ p_process.start
178
+ p_process.wait
179
+
180
+
181
+ # Gather parent and child PIDs
182
+ pids = rewind_and_read(gp_file).split("\n")
183
+ pids.collect! { |pid| pid[/\d+/].to_i }
184
+ c_pid, p_pid = pids
185
+ end
186
+
187
+ # Check that the parent process has dies but the child process is still alive
188
+ expect(alive?(p_pid)).to_not be true
189
+ expect(alive?(c_pid)).to be true
190
+ end
191
+
192
+ it "preserves Dir.pwd in the child" do
193
+ Tempfile.open("dir-spec-out") do |file|
194
+ process = ruby("print Dir.pwd")
195
+ process.io.stdout = process.io.stderr = file
196
+
197
+ expected_dir = nil
198
+ Dir.chdir(Dir.tmpdir) do
199
+ expected_dir = Dir.pwd
200
+ process.start
201
+ end
202
+
203
+ process.wait
204
+
205
+ expect(rewind_and_read(file)).to eq expected_dir
206
+ end
207
+ end
208
+
209
+ it "can handle whitespace, special characters and quotes in arguments" do
210
+ args = ["foo bar", 'foo\bar', "'i-am-quoted'", '"i am double quoted"']
211
+
212
+ Tempfile.open("argv-spec") do |file|
213
+ process = write_argv(file.path, *args).start
214
+ process.wait
215
+
216
+ expect(rewind_and_read(file)).to eq args.inspect
217
+ end
218
+ end
219
+
220
+ it 'handles whitespace in the executable name' do
221
+ path = File.expand_path('foo bar')
222
+
223
+ with_executable_at(path) do |proc|
224
+ expect(proc.start).to eq proc
225
+ expect(proc).to be_alive
226
+ end
227
+ end
228
+
229
+ it "times out when polling for exit" do
230
+ process = sleeping_ruby.start
231
+ expect { process.poll_for_exit(0.1) }.to raise_error(ChildProcess::TimeoutError)
232
+ end
233
+
234
+ it "can change working directory" do
235
+ process = ruby "print Dir.pwd"
236
+
237
+ with_tmpdir { |dir|
238
+ process.cwd = dir
239
+
240
+ orig_pwd = Dir.pwd
241
+
242
+ Tempfile.open('cwd') do |file|
243
+ process.io.stdout = file
244
+
245
+ process.start
246
+ process.wait
247
+
248
+ expect(rewind_and_read(file)).to eq dir
249
+ end
250
+
251
+ expect(Dir.pwd).to eq orig_pwd
252
+ }
253
+ end
254
+
255
+ it 'kills the full process tree', :process_builder => false do
256
+ Tempfile.open('kill-process-tree') do |file|
257
+ process = write_pid_in_sleepy_grand_child(file.path)
258
+ process.leader = true
259
+ process.start
260
+
261
+ pid = wait_until(30) do
262
+ Integer(rewind_and_read(file)) rescue nil
263
+ end
264
+
265
+ process.stop
266
+ wait_until(3) { expect(alive?(pid)).to eql(false) }
267
+ end
268
+ end
269
+
270
+ it 'releases the GIL while waiting for the process' do
271
+ time = Time.now
272
+ threads = []
273
+
274
+ threads << Thread.new { sleeping_ruby(1).start.wait }
275
+ threads << Thread.new(time) { expect(Time.now - time).to be < 0.5 }
276
+
277
+ threads.each { |t| t.join }
278
+ end
279
+
280
+ it 'can check if a detached child is alive' do
281
+ proc = ruby_process("-e", "sleep")
282
+ proc.detach = true
283
+
284
+ proc.start
285
+
286
+ expect(proc).to be_alive
287
+ proc.stop(0)
288
+
289
+ expect(proc).to be_exited
290
+ end
291
+
292
+ describe 'OS detection' do
293
+
294
+ before(:all) do
295
+ # Save off original OS so that it can be restored later
296
+ @original_host_os = RbConfig::CONFIG['host_os']
297
+ end
298
+
299
+ after(:each) do
300
+ # Restore things to the real OS instead of the fake test OS
301
+ RbConfig::CONFIG['host_os'] = @original_host_os
302
+ ChildProcess.instance_variable_set(:@os, nil)
303
+ end
304
+
305
+
306
+ # TODO: add tests for other OSs
307
+ context 'on a BSD system' do
308
+
309
+ let(:bsd_patterns) { ['bsd', 'dragonfly'] }
310
+
311
+ it 'correctly identifies BSD systems' do
312
+ bsd_patterns.each do |pattern|
313
+ RbConfig::CONFIG['host_os'] = pattern
314
+ ChildProcess.instance_variable_set(:@os, nil)
315
+
316
+ expect(ChildProcess.os).to eq(:bsd)
317
+ end
318
+ end
319
+
320
+ end
321
+
322
+ end
323
+
324
+ it 'has a logger' do
325
+ expect(ChildProcess).to respond_to(:logger)
326
+ end
327
+
328
+ it 'can change its logger' do
329
+ expect(ChildProcess).to respond_to(:logger=)
330
+
331
+ original_logger = ChildProcess.logger
332
+ begin
333
+ ChildProcess.logger = :some_other_logger
334
+ expect(ChildProcess.logger).to eq(:some_other_logger)
335
+ ensure
336
+ ChildProcess.logger = original_logger
337
+ end
338
+ end
339
+
340
+
341
+ describe 'logger' do
342
+
343
+ before(:each) do
344
+ ChildProcess.logger = logger
345
+ end
346
+
347
+ after(:all) do
348
+ ChildProcess.logger = nil
349
+ end
350
+
351
+
352
+ context 'with the default logger' do
353
+
354
+ let(:logger) { nil }
355
+
356
+
357
+ it 'logs at INFO level by default' do
358
+ expect(ChildProcess.logger.level).to eq(Logger::INFO)
359
+ end
360
+
361
+ it 'logs at DEBUG level by default if $DEBUG is on' do
362
+ original_debug = $DEBUG
363
+
364
+ begin
365
+ $DEBUG = true
366
+
367
+ expect(ChildProcess.logger.level).to eq(Logger::DEBUG)
368
+ ensure
369
+ $DEBUG = original_debug
370
+ end
371
+ end
372
+
373
+ it "logs to stderr by default" do
374
+ cap = capture_std { generate_log_messages }
375
+
376
+ expect(cap.stdout).to be_empty
377
+ expect(cap.stderr).to_not be_empty
378
+ end
379
+
380
+ end
381
+
382
+ context 'with a custom logger' do
383
+
384
+ let(:logger) { Logger.new($stdout) }
385
+
386
+ it "logs to configured logger" do
387
+ cap = capture_std { generate_log_messages }
388
+
389
+ expect(cap.stdout).to_not be_empty
390
+ expect(cap.stderr).to be_empty
391
+ end
392
+
393
+ end
394
+
395
+ end
396
+
397
+ describe '#started?' do
398
+ subject { process.started? }
399
+
400
+ context 'when not started' do
401
+ let(:process) { sleeping_ruby(1) }
402
+
403
+ it { is_expected.to be false }
404
+ end
405
+
406
+ context 'when started' do
407
+ let(:process) { sleeping_ruby(1).start }
408
+
409
+ it { is_expected.to be true }
410
+ end
411
+
412
+ context 'when finished' do
413
+ before(:each) { process.wait }
414
+
415
+ let(:process) { sleeping_ruby(0).start }
416
+
417
+ it { is_expected.to be true }
418
+ end
419
+
420
+ end
421
+
422
+ end
@@ -0,0 +1,228 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe ChildProcess do
4
+ it "can run even when $stdout is a StringIO" do
5
+ begin
6
+ stdout = $stdout
7
+ $stdout = StringIO.new
8
+ expect { sleeping_ruby.start }.to_not raise_error
9
+ ensure
10
+ $stdout = stdout
11
+ end
12
+ end
13
+
14
+ it "can redirect stdout, stderr" do
15
+ process = ruby(<<-CODE)
16
+ [STDOUT, STDERR].each_with_index do |io, idx|
17
+ io.sync = true
18
+ io.puts idx
19
+ end
20
+ CODE
21
+
22
+ out = Tempfile.new("stdout-spec")
23
+ err = Tempfile.new("stderr-spec")
24
+
25
+ begin
26
+ process.io.stdout = out
27
+ process.io.stderr = err
28
+
29
+ process.start
30
+ expect(process.io.stdin).to be_nil
31
+ process.wait
32
+
33
+ expect(rewind_and_read(out)).to eq "0\n"
34
+ expect(rewind_and_read(err)).to eq "1\n"
35
+ ensure
36
+ out.close
37
+ err.close
38
+ end
39
+ end
40
+
41
+ it "can redirect stdout only" do
42
+ process = ruby(<<-CODE)
43
+ [STDOUT, STDERR].each_with_index do |io, idx|
44
+ io.sync = true
45
+ io.puts idx
46
+ end
47
+ CODE
48
+
49
+ out = Tempfile.new("stdout-spec")
50
+
51
+ begin
52
+ process.io.stdout = out
53
+
54
+ process.start
55
+ process.wait
56
+
57
+ expect(rewind_and_read(out)).to eq "0\n"
58
+ ensure
59
+ out.close
60
+ end
61
+ end
62
+
63
+ it "pumps all output" do
64
+ process = echo
65
+
66
+ out = Tempfile.new("pump")
67
+
68
+ begin
69
+ process.io.stdout = out
70
+
71
+ process.start
72
+ process.wait
73
+
74
+ expect(rewind_and_read(out)).to eq "hello\n"
75
+ ensure
76
+ out.close
77
+ end
78
+ end
79
+
80
+ it "can write to stdin if duplex = true" do
81
+ process = cat
82
+
83
+ out = Tempfile.new("duplex")
84
+ out.sync = true
85
+
86
+ begin
87
+ process.io.stdout = out
88
+ process.io.stderr = out
89
+ process.duplex = true
90
+
91
+ process.start
92
+ process.io.stdin.puts "hello world"
93
+ process.io.stdin.close
94
+
95
+ process.poll_for_exit(exit_timeout)
96
+
97
+ expect(rewind_and_read(out)).to eq "hello world\n"
98
+ ensure
99
+ out.close
100
+ end
101
+ end
102
+
103
+ it "can write to stdin interactively if duplex = true" do
104
+ process = cat
105
+
106
+ out = Tempfile.new("duplex")
107
+ out.sync = true
108
+
109
+ out_receiver = File.open(out.path, "rb")
110
+ begin
111
+ process.io.stdout = out
112
+ process.io.stderr = out
113
+ process.duplex = true
114
+
115
+ process.start
116
+
117
+ stdin = process.io.stdin
118
+
119
+ stdin.puts "hello"
120
+ stdin.flush
121
+ wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\n\z/m) }
122
+
123
+ stdin.putc "n"
124
+ stdin.flush
125
+ wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nn\z/m) }
126
+
127
+ stdin.print "e"
128
+ stdin.flush
129
+ wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nne\z/m) }
130
+
131
+ stdin.printf "w"
132
+ stdin.flush
133
+ wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nnew\z/m) }
134
+
135
+ stdin.write "\nworld\n"
136
+ stdin.flush
137
+ wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nnew\r?\nworld\r?\n\z/m) }
138
+
139
+ stdin.close
140
+ process.poll_for_exit(exit_timeout)
141
+ ensure
142
+ out_receiver.close
143
+ out.close
144
+ end
145
+ end
146
+
147
+ #
148
+ # this works on JRuby 1.6.5 on my Mac, but for some reason
149
+ # hangs on Travis (running 1.6.5.1 + OpenJDK).
150
+ #
151
+ # http://travis-ci.org/#!/enkessler/childprocess/jobs/487331
152
+ #
153
+
154
+ it "works with pipes", :process_builder => false do
155
+ process = ruby(<<-CODE)
156
+ STDOUT.print "stdout"
157
+ STDERR.print "stderr"
158
+ CODE
159
+
160
+ stdout, stdout_w = IO.pipe
161
+ stderr, stderr_w = IO.pipe
162
+
163
+ process.io.stdout = stdout_w
164
+ process.io.stderr = stderr_w
165
+
166
+ process.duplex = true
167
+
168
+ process.start
169
+ process.wait
170
+
171
+ # write streams are closed *after* the process
172
+ # has exited - otherwise it won't work on JRuby
173
+ # with the current Process implementation
174
+
175
+ stdout_w.close
176
+ stderr_w.close
177
+
178
+ out = stdout.read
179
+ err = stderr.read
180
+
181
+ expect([out, err]).to eq %w[stdout stderr]
182
+ end
183
+
184
+ it "can set close-on-exec when IO is inherited" do
185
+ port = random_free_port
186
+ server = TCPServer.new("127.0.0.1", port)
187
+ ChildProcess.close_on_exec server
188
+
189
+ process = sleeping_ruby
190
+ process.io.inherit!
191
+
192
+ process.start
193
+ server.close
194
+
195
+ wait_until { can_bind? "127.0.0.1", port }
196
+ end
197
+
198
+ it "handles long output" do
199
+ process = ruby <<-CODE
200
+ print 'a'*3000
201
+ CODE
202
+
203
+ out = Tempfile.new("long-output")
204
+ out.sync = true
205
+
206
+ begin
207
+ process.io.stdout = out
208
+
209
+ process.start
210
+ process.wait
211
+
212
+ expect(rewind_and_read(out).size).to eq 3000
213
+ ensure
214
+ out.close
215
+ end
216
+ end
217
+
218
+ it 'should not inherit stdout and stderr by default' do
219
+ cap = capture_std do
220
+ process = echo
221
+ process.start
222
+ process.wait
223
+ end
224
+
225
+ expect(cap.stdout).to eq ''
226
+ expect(cap.stderr).to eq ''
227
+ end
228
+ end