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.
- data/README.rdoc +10 -8
- data/lib/right_popen.rb +125 -30
- data/lib/right_popen/process_base.rb +339 -0
- data/lib/right_popen/process_status.rb +64 -0
- data/lib/right_popen/safe_output_buffer.rb +79 -0
- data/lib/right_popen/target_proxy.rb +67 -0
- data/lib/right_popen/version.rb +2 -2
- data/lib/right_popen/win32/popen3_async.rb +258 -0
- data/lib/right_popen/win32/popen3_sync.rb +35 -0
- data/lib/right_popen/win32/process.rb +306 -0
- data/lib/right_popen/win32/{right_popen.rb → right_popen_ex.rb} +8 -231
- data/lib/win32/right_popen.so +0 -0
- data/right_popen.gemspec +2 -2
- data/spec/produce_mixed_output.rb +3 -0
- data/spec/right_popen/safe_output_buffer_spec.rb +26 -0
- data/spec/right_popen_spec.rb +272 -227
- data/spec/runner.rb +171 -79
- data/spec/sleeper.rb +35 -0
- data/spec/stdout.rb +1 -1
- data/spec/writer.rb +34 -0
- metadata +23 -15
data/README.rdoc
CHANGED
@@ -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.
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
data/lib/right_popen.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
#
|
37
|
-
# standard streams of the child process.
|
36
|
+
# see popen3_async for details.
|
38
37
|
#
|
39
|
-
#
|
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
|
-
#
|
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
|
-
#
|
44
|
+
# @return [TrueClass] always true
|
59
45
|
def self.popen3(options)
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|