childprocess 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
1
  rvm:
2
2
  - 1.8.7
3
3
  - 1.9.2
4
- - 1.9.3
4
+ - 1.9.3
5
+ - jruby
data/README.md CHANGED
@@ -14,18 +14,30 @@ Usage
14
14
  ```ruby
15
15
  process = ChildProcess.build("ruby", "-e", "sleep")
16
16
 
17
- # inherit stdout/stderr from parent
17
+ # inherit stdout/stderr from parent...
18
18
  process.io.inherit!
19
19
 
20
- # or pass an IO
20
+ # ...or pass an IO
21
21
  process.io.stdout = Tempfile.new("child-output")
22
22
 
23
+ # start the process
24
+
23
25
  process.start
24
26
 
27
+ # check process status
25
28
  process.alive? #=> true
26
29
  process.exited? #=> false
27
30
 
28
- process.stop
31
+ # wait indefinitely for process to exit...
32
+ process.wait
33
+ process.exited? #=> true
34
+
35
+ # ...or poll for exit + force quit
36
+ begin
37
+ process.poll_for_exit(10)
38
+ rescue ChildProcess::TimeoutError
39
+ process.stop # tries increasingly harsher methods to kill the process.
40
+ end
29
41
  ```
30
42
 
31
43
  The object returned from ChildProcess.build will implement ChildProcess::AbstractProcess.
@@ -44,6 +44,10 @@ module ChildProcess
44
44
  raise SubclassResponsibility, "io"
45
45
  end
46
46
 
47
+ #
48
+ # @return [Fixnum] the pid of the process after it has started
49
+ #
50
+
47
51
  def pid
48
52
  raise SubclassResponsibility, "pid"
49
53
  end
@@ -71,6 +75,16 @@ module ChildProcess
71
75
  raise SubclassResponsibility, "stop"
72
76
  end
73
77
 
78
+ #
79
+ # Block until the process has been terminated.
80
+ #
81
+ # @return [Fixnum] The exit status of the process
82
+ #
83
+
84
+ def wait
85
+ raise SubclassResponsibility, "wait"
86
+ end
87
+
74
88
  #
75
89
  # Did the process exit?
76
90
  #
@@ -3,4 +3,5 @@ module ChildProcess
3
3
  class TimeoutError < StandardError; end
4
4
  class SubclassResponsibility < StandardError; end
5
5
  class InvalidEnvironmentVariableName < StandardError; end
6
+ class LaunchError < StandardError; end
6
7
  end
@@ -4,6 +4,6 @@ module ChildProcess
4
4
  end
5
5
 
6
6
  require "java"
7
- require "childprocess/jruby/redirector"
7
+ require "childprocess/jruby/pump"
8
8
  require "childprocess/jruby/io"
9
9
  require "childprocess/jruby/process"
@@ -3,6 +3,12 @@ require "java"
3
3
  module ChildProcess
4
4
  module JRuby
5
5
  class Process < AbstractProcess
6
+ def initialize(args)
7
+ super(args)
8
+
9
+ @pumps = []
10
+ end
11
+
6
12
  def io
7
13
  @io ||= JRuby::IO.new
8
14
  end
@@ -12,6 +18,8 @@ module ChildProcess
12
18
 
13
19
  assert_started
14
20
  @exit_code = @process.exitValue
21
+ stop_pumps
22
+
15
23
  true
16
24
  rescue java.lang.IllegalThreadStateException
17
25
  false
@@ -23,8 +31,13 @@ module ChildProcess
23
31
  assert_started
24
32
 
25
33
  @process.destroy
26
- @process.waitFor # no way to actually use the timeout here..
34
+ wait # no way to actually use the timeout here..
35
+ end
36
+
37
+ def wait
38
+ @process.waitFor
27
39
 
40
+ stop_pumps
28
41
  @exit_code = @process.exitValue
29
42
  end
30
43
 
@@ -37,7 +50,7 @@ module ChildProcess
37
50
  #
38
51
  def pid
39
52
  if @process.getClass.getName != "java.lang.UNIXProcess"
40
- raise NotImplementedError.new("pid is not supported by JRuby child processes on Windows")
53
+ raise NotImplementedError, "pid is only supported by JRuby child processes on Unix"
41
54
  end
42
55
 
43
56
  # About the best way we can do this is with a nasty reflection-based impl
@@ -56,21 +69,29 @@ module ChildProcess
56
69
  pb.directory java.io.File.new(Dir.pwd)
57
70
  set_env pb.environment
58
71
 
59
- @process = pb.start
72
+ begin
73
+ @process = pb.start
74
+ rescue java.io.IOException => ex
75
+ raise LaunchError, ex.message
76
+ end
77
+
60
78
  setup_io
61
79
  end
62
80
 
63
81
  def setup_io
64
82
  if @io
65
- redirect @process.getErrorStream, @io.stderr
66
- redirect @process.getInputStream, @io.stdout
83
+ @pumps << redirect(@process.getErrorStream, @io.stderr)
84
+ @pumps << redirect(@process.getInputStream, @io.stdout)
67
85
  else
68
86
  @process.getErrorStream.close
69
87
  @process.getInputStream.close
70
88
  end
71
89
 
72
90
  if duplex?
73
- io._stdin = @process.getOutputStream.to_io
91
+ stdin = @process.getOutputStream.to_io
92
+ stdin.sync = true
93
+
94
+ io._stdin = stdin
74
95
  else
75
96
  @process.getOutputStream.close
76
97
  end
@@ -82,8 +103,11 @@ module ChildProcess
82
103
  return
83
104
  end
84
105
 
85
- output = output.to_outputstream
86
- Thread.new { Redirector.new(input, output).run }
106
+ Pump.new(input, output.to_outputstream).run
107
+ end
108
+
109
+ def stop_pumps
110
+ @pumps.each { |pump| pump.stop }
87
111
  end
88
112
 
89
113
  def set_env(env)
@@ -0,0 +1,51 @@
1
+ module ChildProcess
2
+ module JRuby
3
+ class Pump
4
+ BUFFER_SIZE = 2048
5
+
6
+ def initialize(input, output)
7
+ @input = input
8
+ @output = output
9
+ @stop = false
10
+ end
11
+
12
+ def stop
13
+ @stop = true
14
+ @thread && @thread.join
15
+ end
16
+
17
+ def run
18
+ @thread = Thread.new { pump }
19
+
20
+ self
21
+ end
22
+
23
+ private
24
+
25
+ def pump
26
+ buffer = Java.byte[BUFFER_SIZE].new
27
+
28
+ until @stop
29
+ read, avail = 0, 0
30
+
31
+ while read != -1
32
+ avail = [@input.available, 1].max
33
+ read = @input.read(buffer, 0, avail)
34
+
35
+ if read > 0
36
+ @output.write(buffer, 0, read)
37
+ @output.flush
38
+ end
39
+ end
40
+
41
+ sleep 0.1
42
+ end
43
+
44
+ @output.flush
45
+ rescue java.io.IOException => ex
46
+ $stderr.puts ex.message, ex.backtrace if $DEBUG
47
+ end
48
+
49
+ end # Pump
50
+ end # JRuby
51
+ end # ChildProcess
@@ -1,9 +1,6 @@
1
1
  module ChildProcess
2
2
  module Unix
3
3
  class Process < AbstractProcess
4
- #
5
- # @return [Fixnum] the pid of the process after it has started
6
- #
7
4
  attr_reader :pid
8
5
 
9
6
  def io
@@ -28,12 +25,6 @@ module ChildProcess
28
25
  true
29
26
  end
30
27
 
31
- #
32
- # Did the process exit?
33
- #
34
- # @return [Boolean]
35
- #
36
-
37
28
  def exited?
38
29
  return true if @exit_code
39
30
 
@@ -49,12 +40,14 @@ module ChildProcess
49
40
  !!pid
50
41
  end
51
42
 
52
- private
53
-
54
43
  def wait
55
- @exit_code = ::Process.waitpid @pid
44
+ pid, status = ::Process.waitpid2 @pid
45
+
46
+ @exit_code = status.exitstatus || status.termsig
56
47
  end
57
48
 
49
+ private
50
+
58
51
  def send_term
59
52
  send_signal 'TERM'
60
53
  end
@@ -76,11 +69,16 @@ module ChildProcess
76
69
  stderr = @io.stderr
77
70
  end
78
71
 
72
+ # pipe used to detect exec() failure
73
+ exec_r, exec_w = ::IO.pipe
74
+ ChildProcess.close_on_exec exec_w
75
+
79
76
  if duplex?
80
77
  reader, writer = ::IO.pipe
81
78
  end
82
79
 
83
80
  @pid = fork {
81
+ exec_r.close
84
82
  set_env
85
83
 
86
84
  STDOUT.reopen(stdout || "/dev/null")
@@ -91,14 +89,25 @@ module ChildProcess
91
89
  writer.close
92
90
  end
93
91
 
94
- exec(*@args)
92
+ begin
93
+ exec(*@args)
94
+ rescue SystemCallError => ex
95
+ exec_w << ex.message
96
+ end
95
97
  }
96
98
 
99
+ exec_w.close
100
+
97
101
  if duplex?
98
102
  io._stdin = writer
99
103
  reader.close
100
104
  end
101
105
 
106
+ # if we don't eventually get EOF, exec() failed
107
+ unless exec_r.eof?
108
+ raise LaunchError, exec_r.read || "executing command with #{@args.inspect} failed"
109
+ end
110
+
102
111
  ::Process.detach(@pid) if detach?
103
112
  end
104
113
 
@@ -1,3 +1,3 @@
1
1
  module ChildProcess
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
@@ -34,6 +34,9 @@ module ChildProcess
34
34
  read_pipe = read_pipe_ptr.read_pointer
35
35
  write_pipe = write_pipe_ptr.read_pointer
36
36
 
37
+ ok = set_handle_information(write_pipe.address, HANDLE_FLAG_INHERIT, 0)
38
+ ok or raise Error, last_error_message
39
+
37
40
  si[:hStdInput] = read_pipe
38
41
  end
39
42
 
@@ -50,7 +53,7 @@ module ChildProcess
50
53
  pi # process info
51
54
  )
52
55
 
53
- ok or raise Error, last_error_message
56
+ ok or raise LaunchError, last_error_message
54
57
 
55
58
  close_handle pi[:hProcess]
56
59
  close_handle pi[:hThread]
@@ -1,9 +1,7 @@
1
1
  module ChildProcess
2
2
  module Windows
3
3
  class Process < AbstractProcess
4
- #
5
- # @return [Fixnum] the pid of the process after it has started
6
- #
4
+
7
5
  attr_reader :pid
8
6
 
9
7
  def io
@@ -22,6 +20,11 @@ module ChildProcess
22
20
  @handle.close
23
21
  end
24
22
 
23
+ def wait
24
+ @handle.wait
25
+ @exit_code = @handle.exit_code
26
+ end
27
+
25
28
  def exited?
26
29
  return true if @exit_code
27
30
  assert_started
@@ -13,20 +13,32 @@ describe ChildProcess do
13
13
  process.should be_started
14
14
  end
15
15
 
16
+ it "raises ChildProcess::LaunchError if the process can't be started" do
17
+ lambda { invalid_process.start }.should raise_error(ChildProcess::LaunchError)
18
+ end
19
+
16
20
  it "knows if the process crashed" do
17
21
  process = exit_with(1).start
18
- process.poll_for_exit(EXIT_TIMEOUT)
22
+ process.wait
19
23
 
20
24
  process.should be_crashed
21
25
  end
22
26
 
23
27
  it "knows if the process didn't crash" do
24
28
  process = exit_with(0).start
25
- process.poll_for_exit(EXIT_TIMEOUT)
29
+ process.wait
26
30
 
27
31
  process.should_not be_crashed
28
32
  end
29
33
 
34
+ it "can wait for a process to finish" do
35
+ process = exit_with(0).start
36
+ return_value = process.wait
37
+
38
+ process.should_not be_alive
39
+ return_value.should == 0
40
+ end
41
+
30
42
  it "escalates if TERM is ignored" do
31
43
  process = ignored('TERM').start
32
44
  process.stop
@@ -42,7 +54,7 @@ describe ChildProcess do
42
54
  Tempfile.open("env-spec") do |file|
43
55
  with_env('INHERITED' => 'yes') do
44
56
  process = write_env(file.path).start
45
- process.poll_for_exit(EXIT_TIMEOUT)
57
+ process.wait
46
58
  end
47
59
 
48
60
  file.rewind
@@ -59,7 +71,7 @@ describe ChildProcess do
59
71
 
60
72
  ENV['CHILD_ONLY'].should be_nil
61
73
 
62
- process.poll_for_exit(EXIT_TIMEOUT)
74
+ process.wait
63
75
  file.rewind
64
76
 
65
77
  child_env = eval(file.read)
@@ -74,7 +86,8 @@ describe ChildProcess do
74
86
  process.environment['CHILD_ONLY'] = 'yes'
75
87
 
76
88
  process.start
77
- process.poll_for_exit(EXIT_TIMEOUT)
89
+ process.wait
90
+
78
91
  file.rewind
79
92
  child_env = eval(file.read)
80
93
 
@@ -89,7 +102,7 @@ describe ChildProcess do
89
102
 
90
103
  Tempfile.open("argv-spec") do |file|
91
104
  process = write_argv(file.path, *args).start
92
- process.poll_for_exit(EXIT_TIMEOUT)
105
+ process.wait
93
106
 
94
107
  file.rewind
95
108
  file.read.should == args.inspect
@@ -119,7 +132,7 @@ describe ChildProcess do
119
132
 
120
133
  process.start
121
134
  process.io.stdin.should be_nil
122
- process.poll_for_exit(EXIT_TIMEOUT)
135
+ process.wait
123
136
 
124
137
  out.rewind
125
138
  err.rewind
@@ -133,11 +146,10 @@ describe ChildProcess do
133
146
  end
134
147
 
135
148
  it "can write to stdin if duplex = true" do
136
- process = ruby(<<-CODE)
137
- puts(STDIN.gets.chomp)
138
- CODE
149
+ process = cat
139
150
 
140
151
  out = Tempfile.new("duplex")
152
+ out.sync = true
141
153
 
142
154
  begin
143
155
  process.io.stdout = out
@@ -146,7 +158,7 @@ describe ChildProcess do
146
158
 
147
159
  process.start
148
160
  process.io.stdin.puts "hello world"
149
- process.io.stdin.close # JRuby seems to need this
161
+ process.io.stdin.close
150
162
 
151
163
  process.poll_for_exit(EXIT_TIMEOUT)
152
164
 
@@ -177,7 +189,7 @@ describe ChildProcess do
177
189
  process.io.stdout = process.io.stderr = file
178
190
 
179
191
  process.start
180
- process.poll_for_exit(EXIT_TIMEOUT)
192
+ process.wait
181
193
 
182
194
  file.rewind
183
195
  file.read.should == Dir.pwd
@@ -189,11 +201,16 @@ describe ChildProcess do
189
201
 
190
202
  Tempfile.open("argv-spec") do |file|
191
203
  process = write_argv(file.path, *args).start
192
- process.poll_for_exit(EXIT_TIMEOUT)
204
+ process.wait
193
205
 
194
206
  file.rewind
195
207
  file.read.should == args.inspect
196
208
  end
197
209
  end
198
210
 
211
+ it "times out when polling for exit" do
212
+ process = sleeping_ruby.start
213
+ lambda { process.poll_for_exit(0.1) }.should raise_error(ChildProcess::TimeoutError)
214
+ end
215
+
199
216
  end
@@ -4,7 +4,7 @@ shared_examples_for "a platform that provides the child's pid" do
4
4
  it "knows the child's pid" do
5
5
  Tempfile.open("pid-spec") do |file|
6
6
  process = write_pid(file.path).start
7
- process.poll_for_exit(10)
7
+ process.wait
8
8
  file.rewind
9
9
 
10
10
  process.pid.should == file.read.chomp.to_i
@@ -18,6 +18,10 @@ module ChildProcessSpecHelper
18
18
  ruby_process("-e", "sleep")
19
19
  end
20
20
 
21
+ def invalid_process
22
+ @process = ChildProcess.build("unlikely-to-exist")
23
+ end
24
+
21
25
  def ignored(signal)
22
26
  code = <<-RUBY
23
27
  trap(#{signal.inspect}, "IGNORE")
@@ -90,6 +94,19 @@ module ChildProcessSpecHelper
90
94
  raise last_error unless ok
91
95
  end
92
96
 
97
+ def cat
98
+ if ChildProcess.os == :windows
99
+ ruby(<<-CODE)
100
+ STDIN.sync = true
101
+ STDOUT.sync = true
102
+
103
+ puts STDIN.read
104
+ CODE
105
+ else
106
+ ChildProcess.build("cat")
107
+ end
108
+ end
109
+
93
110
  def ruby(code)
94
111
  ruby_process(tmp_script(code))
95
112
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: childprocess
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 31
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 3
10
- version: 0.2.3
9
+ - 4
10
+ version: 0.2.4
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jari Bakken
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-11-27 00:00:00 Z
18
+ date: 2011-12-27 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: rspec
@@ -105,7 +105,7 @@ files:
105
105
  - lib/childprocess/jruby.rb
106
106
  - lib/childprocess/jruby/io.rb
107
107
  - lib/childprocess/jruby/process.rb
108
- - lib/childprocess/jruby/redirector.rb
108
+ - lib/childprocess/jruby/pump.rb
109
109
  - lib/childprocess/unix.rb
110
110
  - lib/childprocess/unix/io.rb
111
111
  - lib/childprocess/unix/process.rb
@@ -1,29 +0,0 @@
1
- module ChildProcess
2
- module JRuby
3
- class Redirector
4
- BUFFER_SIZE = 2048
5
-
6
- def initialize(input, output)
7
- @input = input
8
- @output = output
9
- @buffer = Java.byte[BUFFER_SIZE].new
10
- end
11
-
12
- def run
13
- read, avail = 0, 0
14
-
15
- while(read != -1)
16
- avail = [@input.available, 1].max
17
- read = @input.read(@buffer, 0, avail)
18
-
19
- if read > 0
20
- @output.write(@buffer, 0, read)
21
- end
22
- end
23
- rescue java.io.IOException => ex
24
- $stderr.puts ex.message, ex.backtrace if $DEBUG
25
- end
26
-
27
- end # Redirector
28
- end # JRuby
29
- end # ChildProcess