right_popen 1.1.3 → 3.0.1

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