right_popen 1.0.21-x86-mswin32-60 → 1.1.3-x86-mswin32-60

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
@@ -0,0 +1,339 @@
1
+ #-- -*- mode: ruby; encoding: utf-8 -*-
2
+ # Copyright: 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 NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'thread'
25
+
26
+ module RightScale
27
+ module RightPopen
28
+ class ProcessBase
29
+
30
+ class ProcessError < Exception; end
31
+
32
+ attr_reader :pid, :stdin, :stdout, :stderr, :status_fd, :status
33
+ attr_reader :start_time, :stop_time, :channels_to_finish
34
+
35
+ # === Parameters
36
+ # @param [Hash] options see RightScale.popen3_async for details
37
+ def initialize(options={})
38
+ @options = options
39
+ @stdin = nil
40
+ @stdout = nil
41
+ @stderr = nil
42
+ @status_fd = nil
43
+ @last_interrupt = nil
44
+ @pid = nil
45
+ @start_time = nil
46
+ @stop_time = nil
47
+ @watch_directory = nil
48
+ @size_limit_bytes = nil
49
+ @cmd = nil
50
+ @target = nil
51
+ @status = nil
52
+ @channels_to_finish = nil
53
+ @needs_watching = !!(
54
+ @options[:timeout_seconds] ||
55
+ @options[:size_limit_bytes] ||
56
+ @options[:watch_handler])
57
+ end
58
+
59
+ # Determines if the process is still running.
60
+ #
61
+ # === Return
62
+ # @return [TrueClass|FalseClass] true if running
63
+ def alive?
64
+ raise NotImplementedError, 'Must be overridden'
65
+ end
66
+
67
+ # Determines whether or not to drain all open streams upon death of child
68
+ # or else only those where IO.select indicates data available. This
69
+ # decision is platform-specific.
70
+ #
71
+ # === Return
72
+ # @return [TrueClass|FalseClass] true if draining all
73
+ def drain_all_upon_death?
74
+ raise NotImplementedError, 'Must be overridden'
75
+ end
76
+
77
+ # Determines if this process needs to be watched (beyond waiting for the
78
+ # process to exit).
79
+ #
80
+ # === Return
81
+ # @return [TrueClass|FalseClass] true if needs watching
82
+ def needs_watching?; @needs_watching; end
83
+
84
+ # Determines if timeout on child process has expired, if any.
85
+ #
86
+ # === Return
87
+ # @return [TrueClass|FalseClass] true if timer expired
88
+ def timer_expired?
89
+ !!(@stop_time && Time.now >= @stop_time)
90
+ end
91
+
92
+ # Determines if total size of files created by child process has exceeded
93
+ # the limit specified, if any.
94
+ #
95
+ # === Return
96
+ # @return [TrueClass|FalseClass] true if size limit exceeded
97
+ def size_limit_exceeded?
98
+ if @watch_directory
99
+ globbie = ::File.join(@watch_directory, '**/*')
100
+ size = 0
101
+ ::Dir.glob(globbie) do |f|
102
+ size += ::File.stat(f).size rescue 0 if ::File.file?(f)
103
+ break if size > @size_limit_bytes
104
+ end
105
+ size > @size_limit_bytes
106
+ else
107
+ false
108
+ end
109
+ end
110
+
111
+ # @return [TrueClass|FalseClass] interrupted as true if child process was interrupted by watcher
112
+ def interrupted?; !!@last_interrupt; end
113
+
114
+ # Performs all process operations in synchronous fashion. It is possible
115
+ # for errors or callback behavior to conditionally short-circuit the
116
+ # synchronous operations.
117
+ #
118
+ # === Parameters
119
+ # @param [String|Array] cmd as shell command or binary to execute
120
+ # @param [Object] target that implements all handlers (see TargetProxy)
121
+ #
122
+ # === Return
123
+ # @return [TrueClass] always true
124
+ def sync_all(cmd, target)
125
+ spawn(cmd, target)
126
+ sync_exit_with_target if sync_pid_with_target
127
+ true
128
+ end
129
+
130
+ # Spawns a child process using given command and handler target in a
131
+ # platform-independant manner.
132
+ #
133
+ # must be overridden and override must call super.
134
+ #
135
+ # === Parameters
136
+ # @param [String|Array] cmd as shell command or binary to execute
137
+ # @param [Object] target that implements all handlers (see TargetProxy)
138
+ #
139
+ # === Return
140
+ # @return [TrueClass] always true
141
+ def spawn(cmd, target)
142
+ @cmd = cmd
143
+ @target = target
144
+ @kill_time = nil
145
+ @pid = nil
146
+ @status = nil
147
+ @last_interrupt = nil
148
+ @channels_to_finish = nil
149
+
150
+ if @size_limit_bytes = @options[:size_limit_bytes]
151
+ @watch_directory = @options[:watch_directory] || @options[:directory] || ::Dir.pwd
152
+ end
153
+ end
154
+
155
+ # Performs initial handler callbacks before consuming I/O. Represents any
156
+ # code that must not be invoked twice (unlike sync_exit_with_target).
157
+ #
158
+ # === Return
159
+ # @return [TrueClass|FalseClass] true to begin watch, false to abandon
160
+ def sync_pid_with_target
161
+ # early handling in case caller wants to stream to/from the pipes
162
+ # directly (as in a classic popen3/4 scenario).
163
+ @target.pid_handler(@pid)
164
+ if input_text = @options[:input]
165
+ @stdin.write(input_text)
166
+ end
167
+
168
+ # one-time initialization of the stateful channels_to_finish hash to
169
+ # allow for multiple invocations of the sync_exit_with_target with a
170
+ # possible abandon in between.
171
+ #
172
+ # note that calling IO.select on pipes which have already had all
173
+ # of their output consumed can cause segfault (in Ubuntu?) so it is
174
+ # important to keep track of when all I/O has been consumed.
175
+ @channels_to_finish = [
176
+ [:stdout_handler, @stdout],
177
+ [:stderr_handler, @stderr],
178
+ ]
179
+ @channels_to_finish << [:status_fd, @status_fd] if @status_fd
180
+
181
+ # sync watch_handler has the option to abandon watch as soon as child
182
+ # process comes alive and before streaming any output.
183
+ if @target.watch_handler(self)
184
+ # can close stdin if not returning control to caller.
185
+ @stdin.close rescue nil
186
+ return true
187
+ else
188
+ # caller is reponsible for draining and/or closing all pipes. this can
189
+ # be accomplished by explicity calling either sync_exit_with_target or
190
+ # safe_close_io plus wait_for_exit_status (if data has been read from
191
+ # the I/O streams). it is unsafe to read some data and then call
192
+ # sync_exit_with_target because IO.select may segfault if all of the
193
+ # data in a stream has been already consumed.
194
+ return false
195
+ end
196
+ rescue
197
+ safe_close_io
198
+ raise
199
+ end
200
+
201
+ # Monitors I/O from child process and directly notifies target of any
202
+ # events. Blocks until child exits.
203
+ #
204
+ # === Return
205
+ # @return [TrueClass] always true
206
+ def sync_exit_with_target
207
+ abandon = false
208
+ last_exception = nil
209
+ begin
210
+ while true
211
+ channels_to_watch = @channels_to_finish.map { |ctf| ctf.last }
212
+ ready = ::IO.select(channels_to_watch, nil, nil, 0.1) rescue nil
213
+ dead = !alive?
214
+ channels_to_read = ready && ready.first
215
+ if dead && drain_all_upon_death?
216
+ # finish reading all dead channels.
217
+ channels_to_read = @channels_to_finish.map { |ctf| ctf.last }
218
+ end
219
+ if channels_to_read
220
+ channels_to_read.each do |channel|
221
+ index = @channels_to_finish.index { |ctf| ctf.last == channel }
222
+ key = @channels_to_finish[index].first
223
+ data = dead ? channel.gets(nil) : channel.gets
224
+ if data
225
+ if key == :status_fd
226
+ last_exception = ::Marshal.load(data)
227
+ else
228
+ @target.method(key).call(data)
229
+ end
230
+ else
231
+ # nothing on channel indicates EOF
232
+ @channels_to_finish.delete_at(index)
233
+ end
234
+ end
235
+ end
236
+ if dead
237
+ break
238
+ elsif (interrupted? || timer_expired? || size_limit_exceeded?)
239
+ interrupt
240
+ elsif abandon = !@target.watch_handler(self)
241
+ return true # bypass any remaining callbacks
242
+ end
243
+ end
244
+ wait_for_exit_status
245
+ @target.timeout_handler if timer_expired?
246
+ @target.size_limit_handler if size_limit_exceeded?
247
+ @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
+ ensure
259
+ # abandon will not close I/O objects; caller takes responsibility via
260
+ # process object passed to watch_handler. if anyone calls interrupt
261
+ # then close I/O regardless of abandon to try to force child to die.
262
+ safe_close_io if !abandon || interrupted?
263
+ end
264
+ true
265
+ end
266
+
267
+ # blocks waiting for process exit status.
268
+ #
269
+ # === Return
270
+ # @return [ProcessStatus] exit status
271
+ def wait_for_exit_status
272
+ raise NotImplementedError, 'Must be overridden'
273
+ end
274
+
275
+ # @return [Array] escalating termination signals for this platform
276
+ def signals_for_interrupt
277
+ raise NotImplementedError, 'Must be overridden'
278
+ end
279
+
280
+ # Interrupts the running process (without abandoning watch) in increasing
281
+ # degrees of signalled severity.
282
+ #
283
+ # === Return
284
+ # @return [TrueClass|FalseClass] true if process was alive and interrupted, false if dead before (first) interrupt
285
+ def interrupt
286
+ while alive?
287
+ if !@kill_time || Time.now >= @kill_time
288
+ # soft then hard interrupt (assumed to be called periodically until
289
+ # process is gone).
290
+ sigs = signals_for_interrupt
291
+ if @last_interrupt
292
+ last_index = sigs.index(@last_interrupt)
293
+ next_interrupt = sigs[last_index + 1]
294
+ else
295
+ next_interrupt = sigs.first
296
+ end
297
+ unless next_interrupt
298
+ raise ::RightScale::RightPopen::ProcessBase::ProcessError
299
+ 'Unable to kill child process'
300
+ end
301
+ @last_interrupt = next_interrupt
302
+
303
+ # kill
304
+ result = ::Process.kill(next_interrupt, @pid) rescue nil
305
+ if result
306
+ @kill_time = Time.now + 3 # more seconds until next attempt
307
+ break
308
+ end
309
+ end
310
+ end
311
+ interrupted?
312
+ end
313
+
314
+ # Safely closes any open I/O objects associated with this process.
315
+ #
316
+ # === Return
317
+ # @return [TrueClass] alway true
318
+ def safe_close_io
319
+ @stdin.close rescue nil if @stdin && !@stdin.closed?
320
+ @stdout.close rescue nil if @stdout && !@stdout.closed?
321
+ @stderr.close rescue nil if @stderr && !@stderr.closed?
322
+ @status_fd.close rescue nil if @status_fd && !@status_fd.closed?
323
+ true
324
+ end
325
+
326
+ protected
327
+
328
+ def start_timer
329
+ # start timer when process comes alive (ruby processes are slow to
330
+ # start in Windows, etc.).
331
+ raise ProcessError.new("Process not started") unless @pid
332
+ @start_time = ::Time.now
333
+ @stop_time = @options[:timeout_seconds] ?
334
+ (@start_time + @options[:timeout_seconds]) :
335
+ nil
336
+ end
337
+ end
338
+ end
339
+ end