right_popen 1.0.21 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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