right_popen 1.0.20 → 1.0.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -166,12 +166,17 @@ module RightScale
166
166
  def self.handle_exit(pid, wait_time, handlers, options)
167
167
  EM::Timer.new(wait_time) do
168
168
  if value = Process.waitpid2(pid, Process::WNOHANG)
169
- begin
170
- ignored, status = value
171
- options[:target].method(options[:exit_handler]).call(status) if options[:exit_handler]
172
- ensure
173
- handlers.each { |h| h.drain_and_close }
169
+ ignored, status = value
170
+ first_exception = nil
171
+ handlers.each do |h|
172
+ begin
173
+ h.drain_and_close
174
+ rescue Exception => e
175
+ first_exception = e unless first_exception
176
+ end
174
177
  end
178
+ options[:target].method(options[:exit_handler]).call(status) if options[:exit_handler]
179
+ raise first_exception if first_exception
175
180
  else
176
181
  handle_exit(pid, [wait_time * 2, 1].min, handlers, options)
177
182
  end
@@ -23,6 +23,6 @@
23
23
 
24
24
  module RightScale
25
25
  module RightPopen
26
- VERSION = "1.0.20"
26
+ VERSION = "1.0.21"
27
27
  end
28
28
  end
@@ -1,8 +1,9 @@
1
1
  count = ARGV[0] ? ARGV[0].to_i : 1
2
+ exit_code = ARGV[1] ? ARGV[1].to_i : 0
2
3
 
3
4
  count.times do |i|
4
5
  $stderr.puts "stderr #{i}" if 0 == i % 10
5
6
  $stdout.puts "stdout #{i}"
6
7
  end
7
8
 
8
- exit 99
9
+ exit exit_code
@@ -1,5 +1,6 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
2
  require File.expand_path(File.join(File.dirname(__FILE__), 'runner'))
3
+ require 'stringio'
3
4
 
4
5
  module RightScale
5
6
  describe 'popen3' do
@@ -105,24 +106,62 @@ module RightScale
105
106
  status.pid.should > 0
106
107
  end
107
108
 
108
- it 'should preserve the integrity of stdout and stderr despite interleaving' do
109
- count = LARGE_OUTPUT_COUNTER
110
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{count}"
109
+ it 'should preserve interleaved output when yielding CPU on consumer thread' do
110
+ lines = 11
111
+ exit_code = 42
112
+ repeats = 5
113
+ force_yield = 0.1
114
+ command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{lines} #{exit_code}"
115
+ runner = Runner.new
116
+ actual_output = StringIO.new
117
+ actual_error = StringIO.new
118
+ puts
119
+ stats = runner.run_right_popen(command, :repeats=>repeats, :force_yield=>force_yield) do |status|
120
+ status.status.exitstatus.should == exit_code
121
+ status.pid.should > 0
122
+ actual_output << status.output_text
123
+ actual_error << status.error_text
124
+ print '+'
125
+ end
126
+ puts
127
+ stats.size.should == repeats
128
+
129
+ expected_output = StringIO.new
130
+ repeats.times do
131
+ lines.times do |i|
132
+ expected_output << "stdout #{i}\n"
133
+ end
134
+ end
135
+ actual_output.string.should == expected_output.string
136
+
137
+ expected_error = StringIO.new
138
+ repeats.times do
139
+ lines.times do |i|
140
+ (expected_error << "stderr #{i}\n") if 0 == i % 10
141
+ end
142
+ end
143
+ actual_error.string.should == expected_error.string
144
+ end
145
+
146
+ it 'should preserve interleaved output when process is spewing rapidly' do
147
+ lines = LARGE_OUTPUT_COUNTER
148
+ exit_code = 99
149
+ command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{lines} #{exit_code}"
111
150
  runner = Runner.new
112
151
  status = runner.run_right_popen(command)
113
- status.status.exitstatus.should == 99
152
+ status.status.exitstatus.should == exit_code
114
153
 
115
- results = ''
116
- count.times do |i|
117
- results << "stdout #{i}\n"
154
+ expected_output = StringIO.new
155
+ lines.times do |i|
156
+ expected_output << "stdout #{i}\n"
118
157
  end
119
- status.output_text.should == results
158
+ status.output_text.should == expected_output.string
120
159
 
121
- results = ''
122
- count.times do |i|
123
- (results << "stderr #{i}\n") if 0 == i % 10
160
+ expected_error = StringIO.new
161
+ lines.times do |i|
162
+ (expected_error << "stderr #{i}\n") if 0 == i % 10
124
163
  end
125
- status.error_text.should == results
164
+ status.error_text.should == expected_error.string
126
165
  status.pid.should > 0
127
166
  end
128
167
 
@@ -132,7 +171,7 @@ module RightScale
132
171
  status = runner.run_right_popen(command)
133
172
  status.status.exitstatus.should == 0
134
173
  status.output_text.should_not include('_test_')
135
- status = runner.run_right_popen(command, :__test__ => '42')
174
+ status = runner.run_right_popen(command, :env=>{ :__test__ => '42' })
136
175
  status.status.exitstatus.should == 0
137
176
  status.output_text.should match(/^__test__=42$/)
138
177
  status.pid.should > 0
@@ -145,7 +184,7 @@ module RightScale
145
184
  ENV.each { |k, v| old_envs[k] = v }
146
185
  command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
147
186
  runner = Runner.new
148
- status = runner.run_right_popen(command, :__test__ => '42')
187
+ status = runner.run_right_popen(command, :env=>{ :__test__ => '42' })
149
188
  status.status.exitstatus.should == 0
150
189
  status.output_text.should match(/^__test__=42$/)
151
190
  ENV.each { |k, v| old_envs[k].should == v }
@@ -162,7 +201,7 @@ module RightScale
162
201
  it 'should merge the PATH variable instead of overriding it' do
163
202
  command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
164
203
  runner = Runner.new
165
- status = runner.run_right_popen(command, 'PATH' => "c:/bogus\\bin")
204
+ status = runner.run_right_popen(command, :env=>{ 'PATH' => "c:/bogus\\bin" })
166
205
  status.status.exitstatus.should == 0
167
206
  status.output_text.should include('c:\\bogus\\bin;')
168
207
  status.pid.should > 0
@@ -204,17 +243,19 @@ module RightScale
204
243
  pending 'Set environment variable TEST_LEAK to enable' unless ENV['TEST_LEAK']
205
244
  command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
206
245
  runner = Runner.new
207
- status = runner.run_right_popen(command, nil, nil, REPEAT_TEST_COUNTER)
208
- status.status.exitstatus.should == 0
209
- status.output_text.should == STANDARD_MESSAGE + "\n"
210
- status.error_text.should == ERROR_MESSAGE + "\n"
211
- status.pid.should > 0
246
+ stats = runner.run_right_popen(command, :repeats=>REPEAT_TEST_COUNTER)
247
+ stats.each do |status|
248
+ status.status.exitstatus.should == 0
249
+ status.output_text.should == STANDARD_MESSAGE + "\n"
250
+ status.error_text.should == ERROR_MESSAGE + "\n"
251
+ status.pid.should > 0
252
+ end
212
253
  end
213
254
 
214
255
  it 'should pass input to child process' do
215
256
  command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'increment.rb'))}\""
216
257
  runner = Runner.new
217
- status = runner.run_right_popen(command, nil, "42\n")
258
+ status = runner.run_right_popen(command, :input=>"42\n")
218
259
  status.status.exitstatus.should == 0
219
260
  status.output_text.should == "43\n"
220
261
  status.error_text.should be_empty
@@ -225,7 +266,7 @@ module RightScale
225
266
  pending 'not implemented for windows' if is_windows?
226
267
  command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'stdout.rb'))}\""
227
268
  runner = Runner.new
228
- status = runner.run_right_popen(command, nil, nil)
269
+ status = runner.run_right_popen(command, :expect_timeout=>true, :timeout=>2)
229
270
  status.did_timeout.should be_true
230
271
  status.output_text.should be_empty
231
272
  status.error_text.should == "Closing stdout\n"
@@ -235,7 +276,7 @@ module RightScale
235
276
  pending 'not implemented for windows' if is_windows?
236
277
  command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'background.rb'))}\""
237
278
  runner = Runner.new
238
- status = runner.run_right_popen(command, nil, nil)
279
+ status = runner.run_right_popen(command)
239
280
  status.status.exitstatus.should == 0
240
281
  status.did_timeout.should be_false
241
282
  status.output_text.should be_empty
@@ -24,18 +24,20 @@
24
24
  module RightScale
25
25
  class Runner
26
26
  class RunnerStatus
27
- def initialize(command, block)
27
+ def initialize(command, options={}, &callback)
28
+ options = {:force_yield=>nil, :timeout=>nil, :expect_timeout=>false}.merge(options)
28
29
  @output_text = ""
29
30
  @error_text = ""
30
31
  @status = nil
31
32
  @did_timeout = false
32
- @callback = block
33
+ @callback = callback
33
34
  @pid = nil
35
+ @force_yield = options[:force_yield]
34
36
  EM.next_tick do
35
- @timeout = EM::Timer.new(2) do
36
- puts "\n** Failed to run #{command.inspect}: Timeout"
37
+ @timeout = EM::Timer.new(options[:timeout] || 2) do
38
+ puts "\n** Failed to run #{command.inspect}: Timeout" unless options[:expect_timeout]
37
39
  @did_timeout = true
38
- @callback.call(self)
40
+ @callback.call(self) if options[:expect_timeout]
39
41
  end
40
42
  end
41
43
  end
@@ -43,10 +45,12 @@ module RightScale
43
45
  attr_accessor :output_text, :error_text, :status, :did_timeout, :pid
44
46
 
45
47
  def on_read_stdout(data)
48
+ sleep @force_yield if @force_yield
46
49
  @output_text << data
47
50
  end
48
51
 
49
52
  def on_read_stderr(data)
53
+ sleep @force_yield if @force_yield
50
54
  @error_text << data
51
55
  end
52
56
 
@@ -69,42 +73,51 @@ module RightScale
69
73
  @last_iteration = 0
70
74
  end
71
75
 
72
- def do_right_popen(command, env=nil, input=nil, &callback)
73
- status = RunnerStatus.new(command, callback)
76
+ def do_right_popen(command, options={}, &callback)
77
+ options = {:env=>nil, :input=>nil, :timeout=>nil, :force_yield=>nil, :expect_timeout=>false}.merge(options)
78
+ status = RunnerStatus.new(command, options, &callback)
74
79
  RightScale.popen3(:command => command,
75
- :input => input,
76
- :target => status,
77
- :environment => env,
78
- :stdout_handler => :on_read_stdout,
79
- :stderr_handler => :on_read_stderr,
80
- :pid_handler => :on_pid,
81
- :exit_handler => :on_exit)
80
+ :input => options[:input],
81
+ :target => status,
82
+ :environment => options[:env],
83
+ :stdout_handler => :on_read_stdout,
84
+ :stderr_handler => :on_read_stderr,
85
+ :pid_handler => :on_pid,
86
+ :exit_handler => :on_exit)
82
87
  status
83
88
  end
84
89
 
85
- def run_right_popen(command, env=nil, input=nil, count=1)
90
+ def run_right_popen(command, options={}, &callback)
91
+ options = {:repeats=>1, :env=>nil, :input=>nil, :timeout=>nil, :force_yield=>nil, :expect_timeout=>false}.merge(options)
86
92
  begin
87
93
  @iterations = 0
94
+ @repeats = options[:repeats]
95
+ @stats = []
88
96
  EM.run do
89
- EM.next_tick do
90
- do_right_popen(command, env, input) do |status|
91
- maybe_continue(status)
97
+ EM.defer do
98
+ do_right_popen(command, options) do |status|
99
+ maybe_continue(status, command, options, &callback)
92
100
  end
93
101
  end
94
102
  end
95
- @status
103
+ @stats.size < 2 ? @stats.first : @stats
96
104
  rescue Exception => e
97
105
  puts "\n** Failed: #{e.message} FROM\n#{e.backtrace.join("\n")}"
98
106
  raise e
99
107
  end
100
108
  end
101
109
 
102
- def maybe_continue(status)
110
+ def maybe_continue(status, command, options, &callback)
103
111
  @iterations += 1
104
- if @iterations < @count
105
- do_right_popen(command, env, input) {|status| maybe_continue(status)}
112
+ @stats << status
113
+ callback.call(status) if callback
114
+ if @iterations < @repeats
115
+ EM.defer do
116
+ do_right_popen(command, options) do |status|
117
+ maybe_continue(status, command, options, &callback)
118
+ end
119
+ end
106
120
  else
107
- @status = status
108
121
  EM.stop
109
122
  end
110
123
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_popen
3
3
  version: !ruby/object:Gem::Version
4
- hash: 63
4
+ hash: 61
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
- - 20
10
- version: 1.0.20
9
+ - 21
10
+ version: 1.0.21
11
11
  platform: ruby
12
12
  authors:
13
13
  - Scott Messier
@@ -17,7 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2012-03-07 00:00:00 -08:00
20
+ date: 2012-04-25 00:00:00 -07:00
21
21
  default_executable:
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency
@@ -32,9 +32,9 @@ dependencies:
32
32
  - 12
33
33
  - 11
34
34
  version: 0.12.11
35
+ type: :runtime
35
36
  name: eventmachine
36
37
  prerelease: false
37
- type: :runtime
38
38
  version_requirements: *id001
39
39
  - !ruby/object:Gem::Dependency
40
40
  requirement: &id002 !ruby/object:Gem::Requirement
@@ -47,9 +47,9 @@ dependencies:
47
47
  - 1
48
48
  - 3
49
49
  version: "1.3"
50
+ type: :development
50
51
  name: rspec
51
52
  prerelease: false
52
- type: :development
53
53
  version_requirements: *id002
54
54
  - !ruby/object:Gem::Dependency
55
55
  requirement: &id003 !ruby/object:Gem::Requirement
@@ -63,9 +63,9 @@ dependencies:
63
63
  - 8
64
64
  - 7
65
65
  version: 0.8.7
66
+ type: :development
66
67
  name: rake
67
68
  prerelease: false
68
- type: :development
69
69
  version_requirements: *id003
70
70
  - !ruby/object:Gem::Dependency
71
71
  requirement: &id004 !ruby/object:Gem::Requirement
@@ -77,9 +77,9 @@ dependencies:
77
77
  segments:
78
78
  - 0
79
79
  version: "0"
80
+ type: :development
80
81
  name: flexmock
81
82
  prerelease: false
82
- type: :development
83
83
  version_requirements: *id004
84
84
  description: |
85
85
  RightPopen allows running external processes aynchronously while still