childprocess 0.2.3 → 0.2.4

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