right_popen 1.0.21 → 1.1.3

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.
@@ -15,6 +15,7 @@ documentation.
15
15
  Also use the built-in issues tracker (https://github.com/rightscale/right_popen/issues)
16
16
  to report issues.
17
17
 
18
+ Maintained by the RightScale Teal Team
18
19
 
19
20
  == USAGE
20
21
 
@@ -47,13 +48,14 @@ to report issues.
47
48
  EM.run do
48
49
  EM.next_tick do
49
50
  command = "ruby -e \"puts 'some stdout text'; $stderr.puts 'some stderr text'\; exit 99\""
50
- RightScale.popen3(:command => command,
51
- :target => self,
52
- :environment => nil,
53
- :pid_handler => :on_pid,
54
- :stdout_handler => :on_read_stdout,
55
- :stderr_handler => :on_read_stderr,
56
- :exit_handler => :on_exit)
51
+ RightScale::RightPopen.popen3_async(
52
+ command,
53
+ :target => self,
54
+ :environment => nil,
55
+ :pid_handler => :on_pid,
56
+ :stdout_handler => :on_read_stdout,
57
+ :stderr_handler => :on_read_stderr,
58
+ :exit_handler => :on_exit)
57
59
  end
58
60
  timer = EM::PeriodicTimer.new(0.1) do
59
61
  if @exit_status
@@ -109,7 +111,7 @@ the RightPopen tests:
109
111
 
110
112
  <b>RightPopen</b>
111
113
 
112
- Copyright:: Copyright (c) 2010 RightScale, Inc.
114
+ Copyright:: Copyright (c) 2010-2013 RightScale, Inc.
113
115
 
114
116
  Permission is hereby granted, free of charge, to any person obtaining
115
117
  a copy of this software and associated documentation files (the
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2009 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -21,46 +21,141 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- # RightScale.popen3 allows running external processes aynchronously
25
- # while still capturing their standard and error outputs.
26
- # It relies on EventMachine for most of its internal mechanisms.
24
+ require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'target_proxy'))
27
25
 
28
- if RUBY_PLATFORM =~ /mswin/
29
- require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'win32', 'right_popen'))
30
- else
31
- require File.expand_path(File.join(File.dirname(__FILE__), 'right_popen', 'linux', 'right_popen'))
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
32
  end
33
33
 
34
34
  module RightScale
35
35
 
36
- # Spawn process to run given command asynchronously, hooking all three
37
- # standard streams of the child process.
36
+ # see popen3_async for details.
38
37
  #
39
- # Streams the command's stdout and stderr to the given handlers. Time-
40
- # ordering of bytes sent to stdout and stderr is not preserved.
41
- #
42
- # Calls given exit handler upon command process termination, passing in the
43
- # resulting Process::Status.
44
- #
45
- # All handlers must be methods exposed by the given target.
38
+ # @deprecated in favor of sync vs. async methods in RightPopen namespace.
46
39
  #
47
40
  # === Parameters
48
- # options[:command](String or Array):: Command to execute, including any arguments as a single string or an array of command and arguments
49
- # options[:environment](Hash):: Hash of environment variables values keyed by name
50
- # options[:input](String):: Input string that will get streamed into child's process stdin
51
- # options[:target](Object):: object defining handler methods to be called, optional (no handlers can be defined if not specified)
52
- # options[:pid_handler](String):: PID notification handler method name, optional
53
- # options[:stdout_handler](String):: Stdout handler method name, optional
54
- # options[:stderr_handler](String):: Stderr handler method name, optional
55
- # options[:exit_handler](String):: Exit handler method name, optional
41
+ # @param [Hash] options for execution
56
42
  #
57
43
  # === Returns
58
- # true:: always true
44
+ # @return [TrueClass] always true
59
45
  def self.popen3(options)
60
- raise "EventMachine reactor must be started" unless EM.reactor_running?
61
- raise "Missing command" unless options[:command]
62
- raise "Missing target" unless options[:target] || !options[:stdout_handler] && !options[:stderr_handler] && !options[:exit_handler] && !options[:pid_handler]
63
- return RightScale.popen3_imp(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)
64
51
  end
65
52
 
53
+ module RightPopen
54
+
55
+ # see popen3_async for details.
56
+ DEFAULT_POPEN3_OPTIONS = {
57
+ :environment => nil,
58
+ :exit_handler => nil,
59
+ :group => nil,
60
+ :inherit_io => false,
61
+ :input => nil,
62
+ :locale => true,
63
+ :pid_handler => nil,
64
+ :size_limit_bytes => nil,
65
+ :stderr_handler => nil,
66
+ :stdout_handler => nil,
67
+ :target => nil,
68
+ :timeout_seconds => nil,
69
+ :umask => nil,
70
+ :user => nil,
71
+ :watch_handler => nil,
72
+ :watch_directory => nil,
73
+ }
74
+
75
+ # Loads the specified implementation.
76
+ #
77
+ # === Parameters
78
+ # @param [Symbol|String] synchronicity to load
79
+ #
80
+ # === Return
81
+ # @return [TrueClass] always true
82
+ def self.require_popen3_impl(synchronicity)
83
+ case RUBY_PLATFORM
84
+ when /mswin/
85
+ platform = 'win32'
86
+ when /mingw/
87
+ platform = 'mingw'
88
+ else
89
+ platform = 'linux'
90
+ end
91
+ require ::File.expand_path(::File.join(::File.dirname(__FILE__), 'right_popen', platform, synchronicity.to_s))
92
+ end
93
+
94
+ # Spawns a process to run given command synchronously. This is similar to
95
+ # the Ruby backtick but also supports streaming I/O, process watching, etc.
96
+ # Does not require any evented library to use.
97
+ #
98
+ # Streams the command's stdout and stderr to the given handlers. Time-
99
+ # ordering of bytes sent to stdout and stderr is not preserved.
100
+ #
101
+ # Calls given exit handler upon command process termination, passing in the
102
+ # resulting Process::Status.
103
+ #
104
+ # All handlers must be methods exposed by the given target.
105
+ #
106
+ # === Parameters
107
+ # @param [Hash] options see popen3_async for details
108
+ #
109
+ # === Returns
110
+ # @return [TrueClass] always true
111
+ def self.popen3_sync(cmd, options)
112
+ options = DEFAULT_POPEN3_OPTIONS.dup.merge(options)
113
+ require_popen3_impl(:popen3_sync)
114
+ ::RightScale::RightPopen.popen3_sync_impl(
115
+ cmd, ::RightScale::RightPopen::TargetProxy.new(options), options)
116
+ end
117
+
118
+ # Spawns a process to run given command asynchronously, hooking all three
119
+ # standard streams of the child process. Implementation requires the
120
+ # eventmachine gem.
121
+ #
122
+ # Streams the command's stdout and stderr to the given handlers. Time-
123
+ # ordering of bytes sent to stdout and stderr is not preserved.
124
+ #
125
+ # Calls given exit handler upon command process termination, passing in the
126
+ # resulting Process::Status.
127
+ #
128
+ # All handlers must be methods exposed by the given target.
129
+ #
130
+ # === Parameters
131
+ # @param [Hash] options for execution
132
+ # @option options [Hash] :environment variables values keyed by name
133
+ # @option options [Symbol] :exit_handler target method called on exit
134
+ # @option options [Integer|String] :group or gid for forked process (linux only)
135
+ # @option options [TrueClass|FalseClass] :inherit_io set to true to share all IO objects with forked process or false to close shared IO objects (default) (linux only)
136
+ # @option options [String] :input string that will get streamed into child's process stdin
137
+ # @option options [TrueClass|FalseClass] :locale set to true to export LC_ALL=C in the forked environment (default) or false to use default locale (linux only)
138
+ # @option options [Symbol] :pid_handler target method called with process ID (PID)
139
+ # @option options [Integer] :size_limit_bytes for total size of watched directory after which child process will be interrupted
140
+ # @option options [Symbol] :stderr_handler target method called as error text is received
141
+ # @option options [Symbol] :stdout_handler target method called as output text is received
142
+ # @option options [Object] :target object defining handler methods to be called (no handlers can be defined if not specified)
143
+ # @option options [Numeric] :timeout_seconds after which child process will be interrupted
144
+ # @option options [Integer|String] :umask for files created by process (linux only)
145
+ # @option options [Integer|String] :user or uid for forked process (linux only)
146
+ # @option options [Symbol] :watch_handler called periodically with process during watch; return true to continue, false to abandon (sync only)
147
+ # @option options [String] :watch_directory to monitor for child process writing files
148
+ #
149
+ # === Returns
150
+ # @return [TrueClass] always true
151
+ def self.popen3_async(cmd, options)
152
+ options = DEFAULT_POPEN3_OPTIONS.dup.merge(options)
153
+ require_popen3_impl(:popen3_async)
154
+ unless ::EM.reactor_running?
155
+ raise ::ArgumentError, "EventMachine reactor must be running."
156
+ end
157
+ ::RightScale::RightPopen.popen3_async_impl(
158
+ cmd, ::RightScale::RightPopen::TargetProxy.new(options), options)
159
+ end
160
+ end
66
161
  end
@@ -23,10 +23,13 @@
23
23
 
24
24
  module RightScale
25
25
  module RightPopen
26
+
27
+ # @deprecated this seems like test harness code smell, not production code
26
28
  class Accumulator
27
29
  READ_CHUNK_SIZE = 4096
28
30
 
29
31
  def initialize(process, inputs, read_callbacks, outputs, write_callbacks)
32
+ warn 'WARNING: RightScale::RightPopen::Accumulator is deprecated and will be removed.'
30
33
  @process = process
31
34
  @inputs = inputs
32
35
  @outputs = outputs
@@ -45,10 +48,17 @@ module RightScale
45
48
  @status = nil
46
49
  end
47
50
 
51
+ def status
52
+ unless @status
53
+ @status = ::Process.waitpid2(@process.pid, ::Process::WNOHANG)
54
+ end
55
+ @status
56
+ end
57
+
48
58
  def tick(sleep_time = 0.1)
49
- return true unless @process.status.nil?
59
+ return true unless @status.nil?
50
60
 
51
- @process.status = status = ::Process.waitpid2(@process.pid, ::Process::WNOHANG)
61
+ status
52
62
 
53
63
  inputs = @inputs.dup
54
64
  outputs = @outputs.dup
@@ -93,7 +103,7 @@ module RightScale
93
103
  end
94
104
  end unless ready.nil? || ready[1].nil?
95
105
 
96
- return !@process.status.nil?
106
+ return !@status.nil?
97
107
  end
98
108
 
99
109
  def number_waiting_on
@@ -103,7 +113,7 @@ module RightScale
103
113
  def cleanup
104
114
  @inputs.each {|p| p.close unless p.closed? }
105
115
  @outputs.each {|p| p.close unless p.closed? }
106
- @process.status = ::Process.waitpid2(@process.pid) if @process.status.nil?
116
+ @status = ::Process.waitpid2(@process.pid) if @status.nil?
107
117
  end
108
118
 
109
119
  def run_to_completion(sleep_time=0.1)
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2009 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -21,17 +21,13 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- # RightScale.popen3 allows running external processes aynchronously
25
- # while still capturing their standard and error outputs.
26
- # It relies on EventMachine for most of its internal mechanisms.
27
-
28
24
  require 'rubygems'
29
25
  require 'eventmachine'
26
+
30
27
  require File.expand_path(File.join(File.dirname(__FILE__), "process"))
31
- require File.expand_path(File.join(File.dirname(__FILE__), "accumulator"))
32
- require File.expand_path(File.join(File.dirname(__FILE__), "utilities"))
33
28
 
34
- module RightScale
29
+ module RightScale::RightPopen
30
+
35
31
  # ensure uniqueness of handler to avoid confusion.
36
32
  raise "#{StatusHandler.name} is already defined" if defined?(StatusHandler)
37
33
 
@@ -63,8 +59,8 @@ module RightScale
63
59
 
64
60
  def unbind
65
61
  if @data.size > 0
66
- e = Marshal.load @data
67
- raise (Exception === e ? e : "unknown failure: saw #{e} on status channel")
62
+ e = ::Marshal.load(@data)
63
+ raise (::Exception === e ? e : "unknown failure: saw #{e} on status channel")
68
64
  end
69
65
  end
70
66
  end
@@ -80,11 +76,11 @@ module RightScale
80
76
  # itself.
81
77
  @handle = file_handle
82
78
  @target = target
83
- @handler = handler
79
+ @data_handler = @target.method(handler)
84
80
  end
85
81
 
86
82
  def receive_data(data)
87
- @target.method(@handler).call(data) if @handler
83
+ @data_handler.call(data)
88
84
  end
89
85
 
90
86
  def drain_and_close
@@ -123,62 +119,82 @@ module RightScale
123
119
  end
124
120
  end
125
121
 
126
- # Forks process to run given command asynchronously, hooking all three
127
- # standard streams of the child process.
128
- #
129
- # === Parameters
130
- # options[:pid_handler](Symbol):: Token for pid handler method name.
131
- #
132
- # See RightScale.popen3
133
- def self.popen3_imp(options)
134
- GC.start # To garbage collect open file descriptors from past executions
135
- EM.next_tick do
136
- process = RightPopen::Process.new(:environment => options[:environment] || {})
137
- process.fork(options[:command])
138
-
139
- handlers = []
140
- handlers << EM.attach(process.status_fd, StatusHandler, process.status_fd)
141
- handlers << EM.attach(process.stderr, PipeHandler, process.stderr, options[:target],
142
- options[:stderr_handler])
143
- handlers << EM.attach(process.stdout, PipeHandler, process.stdout, options[:target],
144
- options[:stdout_handler])
145
- handlers << EM.attach(process.stdin, InputHandler, process.stdin, options[:input])
146
-
147
- options[:target].method(options[:pid_handler]).call(process.pid) if options.key? :pid_handler
148
-
149
- handle_exit(process.pid, 0.1, handlers, options)
122
+ # See RightScale.popen3_async for details
123
+ def self.popen3_async_impl(cmd, target, options)
124
+ # always create eventables on the main EM thread by using next_tick. this
125
+ # prevents synchronization problems between EM threads.
126
+ ::EM.next_tick do
127
+ process = nil
128
+ begin
129
+ # create process.
130
+ process = ::RightScale::RightPopen::Process.new(options)
131
+ process.spawn(cmd, target)
132
+
133
+ # connect EM eventables to open streams.
134
+ handlers = []
135
+ handlers << ::EM.attach(process.status_fd, ::RightScale::RightPopen::StatusHandler, process.status_fd)
136
+ handlers << ::EM.attach(process.stderr, ::RightScale::RightPopen::PipeHandler, process.stderr, target, :stderr_handler)
137
+ handlers << ::EM.attach(process.stdout, ::RightScale::RightPopen::PipeHandler, process.stdout, target, :stdout_handler)
138
+ handlers << ::EM.attach(process.stdin, ::RightScale::RightPopen::InputHandler, process.stdin, options[:input])
139
+
140
+ target.pid_handler(process.pid)
141
+
142
+ # initial watch callback.
143
+ #
144
+ # note that we cannot abandon async watch; callback needs to interrupt
145
+ # in this case
146
+ target.watch_handler(process)
147
+
148
+ # periodic watcher.
149
+ watch_process(process, 0.1, target, handlers)
150
+ rescue
151
+ # we can't raise from the main EM thread or it will stop EM.
152
+ # the spawn method will signal the exit handler but not the
153
+ # pid handler in this case since there is no pid. any action
154
+ # (logging, etc.) associated with the failure will have to be
155
+ # driven by the exit handler.
156
+ target.exit_handler(process.status) rescue nil if target && process
157
+ end
150
158
  end
151
159
  true
152
160
  end
153
161
 
154
- # Wait for process to exit and then call exit handler
155
- # If no exit detected, double the wait time up to a maximum of 2 seconds
162
+ # watches process for exit or interrupt criteria. doubles the wait time up to
163
+ # a maximum of 1 second for next wait.
156
164
  #
157
165
  # === Parameters
158
- # pid(Integer):: Process identifier
159
- # wait_time(Fixnum):: Amount of time to wait before checking status
160
- # handlers(Array):: Handlers for status, stderr, stdout, and stdin
161
- # options[:exit_handler](Symbol):: Handler to be called when process exits
162
- # options[:target](Object):: Object initiating command execution
166
+ # @param [Process] process that was run
167
+ # @param [Numeric] wait_time as seconds to wait before checking status
168
+ # @param [Object] target for handler calls
169
+ # @param [Array] handlers used by eventmachine for status, stderr, stdout, and stdin
163
170
  #
164
171
  # === Return
165
172
  # true:: Always return true
166
- def self.handle_exit(pid, wait_time, handlers, options)
167
- EM::Timer.new(wait_time) do
168
- if value = Process.waitpid2(pid, Process::WNOHANG)
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
173
+ def self.watch_process(process, wait_time, target, handlers)
174
+ ::EM::Timer.new(wait_time) do
175
+ begin
176
+ if process.alive?
177
+ if process.timer_expired? || process.size_limit_exceeded?
178
+ process.interrupt
179
+ else
180
+ # cannot abandon async watch; callback needs to interrupt in this case
181
+ target.watch_handler(process)
176
182
  end
183
+ watch_process(process, [wait_time * 2, 1].min, target, handlers)
184
+ else
185
+ handlers.each { |h| h.drain_and_close rescue nil }
186
+ process.wait_for_exit_status
187
+ target.timeout_handler rescue nil if process.timer_expired?
188
+ target.size_limit_handler rescue nil if process.size_limit_exceeded?
189
+ target.exit_handler(process.status) rescue nil
177
190
  end
178
- options[:target].method(options[:exit_handler]).call(status) if options[:exit_handler]
179
- raise first_exception if first_exception
180
- else
181
- handle_exit(pid, [wait_time * 2, 1].min, handlers, options)
191
+ rescue
192
+ # we can't raise from the main EM thread or it will stop EM.
193
+ # the spawn method will signal the exit handler but not the
194
+ # pid handler in this case since there is no pid. any action
195
+ # (logging, etc.) associated with the failure will have to be
196
+ # driven by the exit handler.
197
+ target.exit_handler(process.status) rescue nil if target && process
182
198
  end
183
199
  end
184
200
  true
@@ -0,0 +1,35 @@
1
+ #--
2
+ # Copyright (c) 2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.join(File.dirname(__FILE__), "process"))
25
+
26
+ module RightScale::RightPopen
27
+
28
+ # See RightScale.popen3_sync for details
29
+ def self.popen3_sync_impl(cmd, target, options)
30
+ process = ::RightScale::RightPopen::Process.new(options)
31
+ process.sync_all(cmd, target)
32
+ true
33
+ end
34
+
35
+ end