right_popen 1.1.3 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0c6e583ac6c97d2d520601667148416247a8cbca
4
+ data.tar.gz: ac4c240351ecb997a4b911e1fa6e8a71ca95275a
5
+ SHA512:
6
+ metadata.gz: fceac1055ea55fb2b423fd65b51b380c4a948b7736084f3e2288df6bfa3715276650737fd8505b82cf9eabcb7e79d729b21802b893d95cfcd0b34113fdea8a41
7
+ data.tar.gz: 3f0b2cd13282f1456c1115df88a7a2c8d6a4fdc742cb73faece900fa504c2937758d555603929ad4485ad7afdc36288e816313e9035079c81141a2df5d08554a
@@ -1,5 +1,7 @@
1
1
  = RightPopen
2
2
 
3
+ Maintained by the RightScale Teal Team
4
+
3
5
  == DESCRIPTION
4
6
 
5
7
  === Synopsis
@@ -21,39 +21,20 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'target_proxy'))
25
-
26
- # TEAL FIX: this seems like test harness code smell, not production code. it
27
- # should be removed in next major revision. unfortunately we cannot remove these
28
- # require statements without breaking any code that depends on them.
29
- unless RUBY_PLATFORM =~ /mswin|mingw/
30
- require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'linux', 'accumulator'))
31
- require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'linux', 'utilities'))
32
- end
33
-
34
24
  module RightScale
25
+ module RightPopen
35
26
 
36
- # see popen3_async for details.
37
- #
38
- # @deprecated in favor of sync vs. async methods in RightPopen namespace.
39
- #
40
- # === Parameters
41
- # @param [Hash] options for execution
42
- #
43
- # === Returns
44
- # @return [TrueClass] always true
45
- def self.popen3(options)
46
- warn 'WARNING: RightScale.popen3 is deprecated in favor of RightScale::RightPopen.popen3_async'
47
- options = options.dup
48
- cmd = options.dup.delete(:command)
49
- raise ::ArgumentError.new("Missing command") unless cmd
50
- ::RightScale::RightPopen.popen3_async(options[:command], options)
51
- end
27
+ # exceptions
28
+ class ProcessError < Exception; end
52
29
 
53
- module RightPopen
30
+ # autoloads
31
+ autoload :ProcessStatus, 'right_popen/process_status'
32
+ autoload :SafeOutputBuffer, 'right_popen/safe_output_buffer'
33
+ autoload :TargetProxy, 'right_popen/target_proxy'
54
34
 
55
35
  # see popen3_async for details.
56
36
  DEFAULT_POPEN3_OPTIONS = {
37
+ :directory => nil,
57
38
  :environment => nil,
58
39
  :exit_handler => nil,
59
40
  :group => nil,
@@ -80,15 +61,36 @@ module RightScale
80
61
  # === Return
81
62
  # @return [TrueClass] always true
82
63
  def self.require_popen3_impl(synchronicity)
64
+ # implementation of Process is specific to platform.
83
65
  case RUBY_PLATFORM
84
66
  when /mswin/
85
- platform = 'win32'
67
+ platform_subdir = 'windows'
68
+ impl_subdir = ::File.join(platform_subdir, 'mswin')
86
69
  when /mingw/
87
- platform = 'mingw'
70
+ platform_subdir = 'windows'
71
+ impl_subdir = ::File.join(platform_subdir, 'mingw')
72
+ when /win32|dos|cygwin/
73
+ raise NotImplementedError
88
74
  else
89
- platform = 'linux'
75
+ platform_subdir = 'linux'
76
+ impl_subdir = platform_subdir
90
77
  end
91
- require ::File.expand_path(::File.join(::File.dirname(__FILE__), 'right_popen', platform, synchronicity.to_s))
78
+ impl_module = ::File.join(impl_subdir, 'process')
79
+
80
+ # only require EM when async is requested.
81
+ case synchronicity
82
+ when :popen3_sync
83
+ sync_module = 'popen3_sync'
84
+ when :popen3_async
85
+ sync_module = ::File.join(platform_subdir, 'popen3_async')
86
+ else
87
+ fail 'unexpected synchronicity'
88
+ end
89
+
90
+ # platform-specific requires.
91
+ base_dir = ::File.join(::File.dirname(__FILE__), 'right_popen').gsub("\\", '/')
92
+ require ::File.expand_path(impl_module, base_dir)
93
+ require ::File.expand_path(sync_module, base_dir)
92
94
  end
93
95
 
94
96
  # Spawns a process to run given command synchronously. This is similar to
@@ -129,6 +131,7 @@ module RightScale
129
131
  #
130
132
  # === Parameters
131
133
  # @param [Hash] options for execution
134
+ # @option options [String] :directory as initial working directory for child process or nil to inherit current working directory
132
135
  # @option options [Hash] :environment variables values keyed by name
133
136
  # @option options [Symbol] :exit_handler target method called on exit
134
137
  # @option options [Integer|String] :group or gid for forked process (linux only)
@@ -145,6 +148,7 @@ module RightScale
145
148
  # @option options [Integer|String] :user or uid for forked process (linux only)
146
149
  # @option options [Symbol] :watch_handler called periodically with process during watch; return true to continue, false to abandon (sync only)
147
150
  # @option options [String] :watch_directory to monitor for child process writing files
151
+ # @option options [Symbol] :async_exception_handler target method called if an exception is handled (on another thread)
148
152
  #
149
153
  # === Returns
150
154
  # @return [TrueClass] always true
@@ -22,9 +22,9 @@
22
22
  #++
23
23
 
24
24
  require 'rubygems'
25
+ require 'right_popen'
25
26
  require 'eventmachine'
26
-
27
- require File.expand_path(File.join(File.dirname(__FILE__), "process"))
27
+ require 'yaml'
28
28
 
29
29
  module RightScale::RightPopen
30
30
 
@@ -32,12 +32,13 @@ module RightScale::RightPopen
32
32
  raise "#{StatusHandler.name} is already defined" if defined?(StatusHandler)
33
33
 
34
34
  module StatusHandler
35
- def initialize(file_handle)
35
+ def initialize(file_handle, target)
36
36
  # Voodoo to make sure that Ruby doesn't gc the file handle
37
37
  # (closing the stream) before we're done with it. No, oddly
38
38
  # enough EventMachine is not good about holding on to this
39
39
  # itself.
40
40
  @handle = file_handle
41
+ @target = target
41
42
  @data = ""
42
43
  end
43
44
 
@@ -59,8 +60,15 @@ module RightScale::RightPopen
59
60
 
60
61
  def unbind
61
62
  if @data.size > 0
62
- e = ::Marshal.load(@data)
63
- raise (::Exception === e ? e : "unknown failure: saw #{e} on status channel")
63
+ error_data = ::YAML.load(@data)
64
+ status_fd_error = ::RightScale::RightPopen::ProcessError.new(
65
+ "#{error_data['class']}: #{error_data['message']}")
66
+ if error_data['backtrace']
67
+ status_fd_error.set_backtrace(error_data['backtrace'])
68
+ end
69
+ if @target
70
+ @target.async_exception_handler(status_fd_error) rescue nil
71
+ end
64
72
  end
65
73
  end
66
74
  end
@@ -132,7 +140,7 @@ module RightScale::RightPopen
132
140
 
133
141
  # connect EM eventables to open streams.
134
142
  handlers = []
135
- handlers << ::EM.attach(process.status_fd, ::RightScale::RightPopen::StatusHandler, process.status_fd)
143
+ handlers << ::EM.attach(process.status_fd, ::RightScale::RightPopen::StatusHandler, process.status_fd, target)
136
144
  handlers << ::EM.attach(process.stderr, ::RightScale::RightPopen::PipeHandler, process.stderr, target, :stderr_handler)
137
145
  handlers << ::EM.attach(process.stdout, ::RightScale::RightPopen::PipeHandler, process.stdout, target, :stdout_handler)
138
146
  handlers << ::EM.attach(process.stdin, ::RightScale::RightPopen::InputHandler, process.stdin, options[:input])
@@ -147,13 +155,16 @@ module RightScale::RightPopen
147
155
 
148
156
  # periodic watcher.
149
157
  watch_process(process, 0.1, target, handlers)
150
- rescue
158
+ rescue Exception => e
151
159
  # we can't raise from the main EM thread or it will stop EM.
152
160
  # the spawn method will signal the exit handler but not the
153
161
  # pid handler in this case since there is no pid. any action
154
162
  # (logging, etc.) associated with the failure will have to be
155
163
  # driven by the exit handler.
156
- target.exit_handler(process.status) rescue nil if target && process
164
+ if target
165
+ target.async_exception_handler(e) rescue nil
166
+ target.exit_handler(process.status) rescue nil if process
167
+ end
157
168
  end
158
169
  end
159
170
  true
@@ -188,13 +199,18 @@ module RightScale::RightPopen
188
199
  target.size_limit_handler rescue nil if process.size_limit_exceeded?
189
200
  target.exit_handler(process.status) rescue nil
190
201
  end
191
- rescue
202
+ rescue Exception => e
192
203
  # we can't raise from the main EM thread or it will stop EM.
193
204
  # the spawn method will signal the exit handler but not the
194
205
  # pid handler in this case since there is no pid. any action
195
206
  # (logging, etc.) associated with the failure will have to be
196
207
  # driven by the exit handler.
197
- target.exit_handler(process.status) rescue nil if target && process
208
+ if target
209
+ target.async_exception_handler(e) rescue nil
210
+ status = process && process.status
211
+ status ||= ::RightScale::RightPopen::ProcessStatus.new(nil, 1)
212
+ target.exit_handler(status)
213
+ end
198
214
  end
199
215
  end
200
216
  true
@@ -21,14 +21,16 @@
21
21
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ require 'rubygems'
24
25
  require 'etc'
25
-
26
- require ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', 'process_base'))
27
- require ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', 'process_status'))
26
+ require 'fcntl'
27
+ require 'yaml'
28
+ require 'right_popen'
29
+ require 'right_popen/process_base'
28
30
 
29
31
  module RightScale
30
32
  module RightPopen
31
- class Process < ProcessBase
33
+ class Process < ::RightScale::RightPopen::ProcessBase
32
34
 
33
35
  def initialize(options={})
34
36
  super(options)
@@ -39,7 +41,9 @@ module RightScale
39
41
  # === Return
40
42
  # @return [TrueClass|FalseClass] true if running
41
43
  def alive?
42
- raise ProcessError.new('Process not started') unless @pid
44
+ unless @pid
45
+ raise ::RightScale::RightPopen::ProcessError, 'Process not started'
46
+ end
43
47
  unless @status
44
48
  begin
45
49
  ignored, status = ::Process.waitpid2(@pid, ::Process::WNOHANG)
@@ -72,7 +76,9 @@ module RightScale
72
76
  # === Return
73
77
  # @return [ProcessStatus] exit status
74
78
  def wait_for_exit_status
75
- raise ProcessError.new('Process not started') unless @pid
79
+ unless @pid
80
+ raise ::RightScale::RightPopen::ProcessError, 'Process not started'
81
+ end
76
82
  unless @status
77
83
  begin
78
84
  ignored, status = ::Process.waitpid2(@pid)
@@ -162,7 +168,7 @@ module RightScale
162
168
  environment_hash['LC_ALL'] = 'C' if @options[:locale]
163
169
  environment_hash.merge!(@options[:environment]) if @options[:environment]
164
170
  environment_hash.each do |key, value|
165
- ::ENV[key.to_s] = value.to_s if value
171
+ ::ENV[key.to_s] = value.nil? ? nil: value.to_s
166
172
  end
167
173
 
168
174
  if cmd.kind_of?(Array)
@@ -173,7 +179,14 @@ module RightScale
173
179
  end
174
180
  raise 'Unreachable code'
175
181
  rescue ::Exception => e
176
- ::Marshal.dump(e, status_w)
182
+ # note that Marshal.dump/load isn't reliable for all kinds of
183
+ # exceptions or else can be truncated by I/O buffering.
184
+ error_data = {
185
+ 'class' => e.class.name,
186
+ 'message' => e.message,
187
+ 'backtrace' => e.backtrace
188
+ }
189
+ status_w.puts(::YAML.dump(error_data))
177
190
  end
178
191
  status_w.close
179
192
  exit!
@@ -197,23 +210,6 @@ module RightScale
197
210
  raise
198
211
  end
199
212
 
200
- # @deprecated this seems like test harness code smell, not production code.
201
- def wait_for_exec
202
- warn 'WARNING: RightScale::RightPopen::Process#wait_for_exec is deprecated in lib and will be moved to spec'
203
- begin
204
- e = ::Marshal.load(@status_fd)
205
- # thus meaning that the process failed to exec...
206
- @stdin.close
207
- @stdout.close
208
- @stderr.close
209
- raise(Exception === e ? e : "unknown failure!")
210
- rescue EOFError
211
- # thus meaning that the process did exec and we can continue.
212
- ensure
213
- @status_fd.close
214
- end
215
- end
216
-
217
213
  private
218
214
 
219
215
  def get_user
@@ -21,7 +21,8 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- require File.expand_path(File.join(File.dirname(__FILE__), "process"))
24
+ require 'rubygems'
25
+ require 'right_popen'
25
26
 
26
27
  module RightScale::RightPopen
27
28
 
@@ -21,14 +21,15 @@
21
21
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ require 'rubygems'
25
+ require 'right_popen'
24
26
  require 'thread'
27
+ require 'yaml'
25
28
 
26
29
  module RightScale
27
30
  module RightPopen
28
31
  class ProcessBase
29
32
 
30
- class ProcessError < Exception; end
31
-
32
33
  attr_reader :pid, :stdin, :stdout, :stderr, :status_fd, :status
33
34
  attr_reader :start_time, :stop_time, :channels_to_finish
34
35
 
@@ -146,6 +147,7 @@ module RightScale
146
147
  @status = nil
147
148
  @last_interrupt = nil
148
149
  @channels_to_finish = nil
150
+ @wait_thread = nil
149
151
 
150
152
  if @size_limit_bytes = @options[:size_limit_bytes]
151
153
  @watch_directory = @options[:watch_directory] || @options[:directory] || ::Dir.pwd
@@ -205,7 +207,7 @@ module RightScale
205
207
  # @return [TrueClass] always true
206
208
  def sync_exit_with_target
207
209
  abandon = false
208
- last_exception = nil
210
+ status_fd_data = []
209
211
  begin
210
212
  while true
211
213
  channels_to_watch = @channels_to_finish.map { |ctf| ctf.last }
@@ -223,7 +225,7 @@ module RightScale
223
225
  data = dead ? channel.gets(nil) : channel.gets
224
226
  if data
225
227
  if key == :status_fd
226
- last_exception = ::Marshal.load(data)
228
+ status_fd_data << data
227
229
  else
228
230
  @target.method(key).call(data)
229
231
  end
@@ -242,19 +244,19 @@ module RightScale
242
244
  end
243
245
  end
244
246
  wait_for_exit_status
247
+ unless status_fd_data.empty?
248
+ data = status_fd_data.join
249
+ error_data = ::YAML.load(data)
250
+ status_fd_error = ::RightScale::RightPopen::ProcessError.new(
251
+ "#{error_data['class']}: #{error_data['message']}")
252
+ if error_data['backtrace']
253
+ status_fd_error.set_backtrace(error_data['backtrace'])
254
+ end
255
+ raise status_fd_error
256
+ end
245
257
  @target.timeout_handler if timer_expired?
246
258
  @target.size_limit_handler if size_limit_exceeded?
247
259
  @target.exit_handler(@status)
248
-
249
- # re-raise exception from fork, if any.
250
- case last_exception
251
- when nil
252
- # all good
253
- when ::Exception
254
- raise last_exception
255
- else
256
- raise "Unknown failure: saw #{last_exception.inspect} on status channel."
257
- end
258
260
  ensure
259
261
  # abandon will not close I/O objects; caller takes responsibility via
260
262
  # process object passed to watch_handler. if anyone calls interrupt
@@ -295,7 +297,7 @@ module RightScale
295
297
  next_interrupt = sigs.first
296
298
  end
297
299
  unless next_interrupt
298
- raise ::RightScale::RightPopen::ProcessBase::ProcessError
300
+ raise ::RightScale::RightPopen::ProcessError,
299
301
  'Unable to kill child process'
300
302
  end
301
303
  @last_interrupt = next_interrupt
@@ -328,7 +330,9 @@ module RightScale
328
330
  def start_timer
329
331
  # start timer when process comes alive (ruby processes are slow to
330
332
  # start in Windows, etc.).
331
- raise ProcessError.new("Process not started") unless @pid
333
+ unless @pid
334
+ raise ::RightScale::RightPopen::ProcessError, 'Process not started'
335
+ end
332
336
  @start_time = ::Time.now
333
337
  @stop_time = @options[:timeout_seconds] ?
334
338
  (@start_time + @options[:timeout_seconds]) :
@@ -21,6 +21,9 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ require 'rubygems'
25
+ require 'right_popen'
26
+
24
27
  module RightScale
25
28
 
26
29
  module RightPopen
@@ -21,6 +21,9 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ require 'rubygems'
25
+ require 'right_popen'
26
+
24
27
  module RightScale
25
28
 
26
29
  module RightPopen
@@ -21,6 +21,9 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ require 'rubygems'
25
+ require 'right_popen'
26
+
24
27
  module RightScale
25
28
  module RightPopen
26
29
 
@@ -29,19 +32,20 @@ module RightScale
29
32
  class TargetProxy
30
33
 
31
34
  HANDLER_NAME_TO_PARAMETER_COUNT = {
32
- :exit_handler => 1,
33
- :pid_handler => 1,
34
- :size_limit_handler => 0,
35
- :stderr_handler => 1,
36
- :stdout_handler => 1,
37
- :timeout_handler => 0,
38
- :watch_handler => 1,
35
+ :exit_handler => 1,
36
+ :pid_handler => 1,
37
+ :size_limit_handler => 0,
38
+ :stderr_handler => 1,
39
+ :stdout_handler => 1,
40
+ :timeout_handler => 0,
41
+ :watch_handler => 1,
42
+ :async_exception_handler => 1,
39
43
  }
40
44
 
41
45
  def initialize(options = {})
42
46
  if options[:target].nil? &&
43
47
  !(options.keys & HANDLER_NAME_TO_PARAMETER_COUNT.keys).empty?
44
- raise ArgumentError, "Missing target"
48
+ raise ::ArgumentError, 'Missing target'
45
49
  end
46
50
  @target = options[:target] # hold target reference (if any) against GC
47
51