childprocess 0.9.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 +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