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.
- data/README.rdoc +10 -8
- data/lib/right_popen.rb +125 -30
- data/lib/right_popen/linux/accumulator.rb +14 -4
- data/lib/right_popen/linux/{right_popen.rb → popen3_async.rb} +73 -57
- data/lib/right_popen/linux/popen3_sync.rb +35 -0
- data/lib/right_popen/linux/process.rb +136 -44
- data/lib/right_popen/linux/utilities.rb +5 -2
- 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/right_popen.gemspec +2 -2
- data/spec/produce_mixed_output.rb +3 -0
- data/spec/right_popen/linux/accumulator_spec.rb +5 -13
- 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 +32 -26
@@ -1,5 +1,5 @@
|
|
1
1
|
#-- -*- mode: ruby; encoding: utf-8 -*-
|
2
|
-
# Copyright: Copyright (c) 2011 RightScale, Inc.
|
2
|
+
# Copyright: Copyright (c) 2011-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
|
@@ -23,20 +23,87 @@
|
|
23
23
|
|
24
24
|
require 'etc'
|
25
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'))
|
28
|
+
|
26
29
|
module RightScale
|
27
30
|
module RightPopen
|
28
|
-
class Process
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
class Process < ProcessBase
|
32
|
+
|
33
|
+
def initialize(options={})
|
34
|
+
super(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Determines if the process is still running.
|
38
|
+
#
|
39
|
+
# === Return
|
40
|
+
# @return [TrueClass|FalseClass] true if running
|
41
|
+
def alive?
|
42
|
+
raise ProcessError.new('Process not started') unless @pid
|
43
|
+
unless @status
|
44
|
+
begin
|
45
|
+
ignored, status = ::Process.waitpid2(@pid, ::Process::WNOHANG)
|
46
|
+
@status = status
|
47
|
+
rescue
|
48
|
+
wait_for_exit_status
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@status.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Linux must only read streams that are selected for read, even on child
|
55
|
+
# death. the issue is that a child process can (inexplicably) close one of
|
56
|
+
# the streams but continue writing to the other and this will cause the
|
57
|
+
# parent to hang reading the stream until the child goes away.
|
58
|
+
#
|
59
|
+
# === Return
|
60
|
+
# @return [TrueClass|FalseClass] true if draining all
|
61
|
+
def drain_all_upon_death?
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Array] escalating termination signals for this platform
|
66
|
+
def signals_for_interrupt
|
67
|
+
['INT', 'TERM', 'KILL']
|
36
68
|
end
|
37
69
|
|
38
|
-
|
39
|
-
|
70
|
+
# blocks waiting for process exit status.
|
71
|
+
#
|
72
|
+
# === Return
|
73
|
+
# @return [ProcessStatus] exit status
|
74
|
+
def wait_for_exit_status
|
75
|
+
raise ProcessError.new('Process not started') unless @pid
|
76
|
+
unless @status
|
77
|
+
begin
|
78
|
+
ignored, status = ::Process.waitpid2(@pid)
|
79
|
+
@status = status
|
80
|
+
rescue
|
81
|
+
# ignored
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@status
|
85
|
+
end
|
86
|
+
|
87
|
+
# spawns (forks) a child process using given command and handler target in
|
88
|
+
# linux-specific manner.
|
89
|
+
#
|
90
|
+
# must be overridden and override must call super.
|
91
|
+
#
|
92
|
+
# === Parameters
|
93
|
+
# @param [String|Array] cmd as shell command or binary to execute
|
94
|
+
# @param [Object] target that implements all handlers (see TargetProxy)
|
95
|
+
#
|
96
|
+
# === Return
|
97
|
+
# @return [TrueClass] always true
|
98
|
+
def spawn(cmd, target)
|
99
|
+
super(cmd, target)
|
100
|
+
|
101
|
+
# garbage collect any open file descriptors from past executions before
|
102
|
+
# forking to prevent them being inherited. also reduces memory footprint
|
103
|
+
# since forking will duplicate everything in memory for child process.
|
104
|
+
::GC.start
|
105
|
+
|
106
|
+
# create pipes.
|
40
107
|
stdin_r, stdin_w = IO.pipe
|
41
108
|
stdout_r, stdout_w = IO.pipe
|
42
109
|
stderr_r, stderr_w = IO.pipe
|
@@ -45,24 +112,28 @@ module RightScale
|
|
45
112
|
[stdin_r, stdin_w, stdout_r, stdout_w,
|
46
113
|
stderr_r, stderr_w, status_r, status_w].each {|fdes| fdes.sync = true}
|
47
114
|
|
48
|
-
@pid = Kernel::fork do
|
115
|
+
@pid = ::Kernel::fork do
|
49
116
|
begin
|
50
117
|
stdin_w.close
|
51
|
-
STDIN.reopen stdin_r
|
118
|
+
::STDIN.reopen stdin_r
|
52
119
|
|
53
120
|
stdout_r.close
|
54
|
-
STDOUT.reopen stdout_w
|
121
|
+
::STDOUT.reopen stdout_w
|
55
122
|
|
56
123
|
stderr_r.close
|
57
|
-
STDERR.reopen stderr_w
|
124
|
+
::STDERR.reopen stderr_w
|
58
125
|
|
59
126
|
status_r.close
|
60
|
-
status_w.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
127
|
+
status_w.fcntl(::Fcntl::F_SETFD, ::Fcntl::FD_CLOEXEC)
|
128
|
+
|
129
|
+
unless @options[:inherit_io]
|
130
|
+
::ObjectSpace.each_object(IO) do |io|
|
131
|
+
if ![::STDIN, ::STDOUT, ::STDERR, status_w].include?(io)
|
132
|
+
# be careful to not allow streams in a bad state from the
|
133
|
+
# parent process to prevent child process running.
|
134
|
+
(io.close rescue nil) unless (io.closed? rescue true)
|
135
|
+
end
|
136
|
+
end
|
66
137
|
end
|
67
138
|
|
68
139
|
if group = get_group
|
@@ -75,24 +146,34 @@ module RightScale
|
|
75
146
|
::Process.uid = user
|
76
147
|
end
|
77
148
|
|
78
|
-
|
79
|
-
|
80
|
-
|
149
|
+
if umask = get_umask
|
150
|
+
::File.umask(umask)
|
151
|
+
end
|
81
152
|
|
82
|
-
|
83
|
-
|
84
|
-
|
153
|
+
# avoid chdir when pwd is already correct due to asinine printed
|
154
|
+
# warning from chdir block for what is basically a no-op.
|
155
|
+
working_directory = @options[:directory]
|
156
|
+
if working_directory &&
|
157
|
+
::File.expand_path(working_directory) != ::File.expand_path(::Dir.pwd)
|
158
|
+
::Dir.chdir(working_directory)
|
159
|
+
end
|
85
160
|
|
86
|
-
|
161
|
+
environment_hash = {}
|
162
|
+
environment_hash['LC_ALL'] = 'C' if @options[:locale]
|
163
|
+
environment_hash.merge!(@options[:environment]) if @options[:environment]
|
164
|
+
environment_hash.each do |key, value|
|
165
|
+
::ENV[key.to_s] = value.to_s if value
|
166
|
+
end
|
87
167
|
|
88
168
|
if cmd.kind_of?(Array)
|
169
|
+
cmd = cmd.map { |c| c.to_s } #exec only likes string arguments
|
89
170
|
exec(*cmd)
|
90
171
|
else
|
91
|
-
exec(
|
172
|
+
exec('sh', '-c', cmd.to_s) # allows shell commands for cmd string
|
92
173
|
end
|
93
|
-
raise '
|
94
|
-
rescue Exception => e
|
95
|
-
Marshal.dump(e, status_w)
|
174
|
+
raise 'Unreachable code'
|
175
|
+
rescue ::Exception => e
|
176
|
+
::Marshal.dump(e, status_w)
|
96
177
|
end
|
97
178
|
status_w.close
|
98
179
|
exit!
|
@@ -106,11 +187,21 @@ module RightScale
|
|
106
187
|
@stdout = stdout_r
|
107
188
|
@stderr = stderr_r
|
108
189
|
@status_fd = status_r
|
190
|
+
start_timer
|
191
|
+
true
|
192
|
+
rescue
|
193
|
+
# catch-all for failure to spawn process ensuring a non-nil status. the
|
194
|
+
# PID most likely is nil but the exit handler can be invoked for async.
|
195
|
+
safe_close_io
|
196
|
+
@status = ::RightScale::RightPopen::ProcessStatus.new(@pid, 1)
|
197
|
+
raise
|
109
198
|
end
|
110
199
|
|
200
|
+
# @deprecated this seems like test harness code smell, not production code.
|
111
201
|
def wait_for_exec
|
202
|
+
warn 'WARNING: RightScale::RightPopen::Process#wait_for_exec is deprecated in lib and will be moved to spec'
|
112
203
|
begin
|
113
|
-
e = Marshal.load
|
204
|
+
e = ::Marshal.load(@status_fd)
|
114
205
|
# thus meaning that the process failed to exec...
|
115
206
|
@stdin.close
|
116
207
|
@stdout.close
|
@@ -126,28 +217,29 @@ module RightScale
|
|
126
217
|
private
|
127
218
|
|
128
219
|
def get_user
|
129
|
-
user = @
|
130
|
-
|
131
|
-
user = Etc.getpwnam(user).uid if user
|
220
|
+
if user = @options[:user]
|
221
|
+
user = Etc.getpwnam(user).uid unless user.kind_of?(Integer)
|
132
222
|
end
|
133
223
|
user
|
134
224
|
end
|
135
225
|
|
136
226
|
def get_group
|
137
|
-
group = @
|
138
|
-
|
139
|
-
group = Etc.getgrnam(group).gid if group
|
227
|
+
if group = @options[:group]
|
228
|
+
group = Etc.getgrnam(group).gid unless group.kind_of?(Integer)
|
140
229
|
end
|
141
230
|
group
|
142
231
|
end
|
143
232
|
|
144
233
|
def get_umask
|
145
|
-
if @
|
146
|
-
|
147
|
-
|
148
|
-
|
234
|
+
if umask = @options[:umask]
|
235
|
+
if umask.respond_to?(:oct)
|
236
|
+
umask = umask.oct
|
237
|
+
else
|
238
|
+
umask = umask.to_i
|
239
|
+
end
|
240
|
+
umask = umask & 007777
|
149
241
|
end
|
150
|
-
|
242
|
+
umask
|
151
243
|
end
|
152
244
|
end
|
153
245
|
end
|
@@ -26,6 +26,8 @@ require File.expand_path(File.join(File.dirname(__FILE__), "accumulator"))
|
|
26
26
|
|
27
27
|
module RightScale
|
28
28
|
module RightPopen
|
29
|
+
|
30
|
+
# @deprecated this seems like test harness code smell, not production code
|
29
31
|
module Utilities
|
30
32
|
module_function
|
31
33
|
|
@@ -77,14 +79,15 @@ module RightScale
|
|
77
79
|
alias_method :spawn, :run_collecting_output
|
78
80
|
|
79
81
|
def run_with_blocks(cmd, stdin_block, stdout_block, stderr_block, parameters={})
|
82
|
+
warn 'WARNING: RightScale::RightPopen::Utilities are deprecated and will be removed.'
|
80
83
|
process = Process.new(parameters)
|
81
|
-
process.
|
84
|
+
process.spawn(cmd, ::RightScale::RightPopen::TargetProxy.new(parameters))
|
82
85
|
process.wait_for_exec
|
83
86
|
a = Accumulator.new(process,
|
84
87
|
[process.stdout, process.stderr], [stdout_block, stderr_block],
|
85
88
|
[process.stdin], [stdin_block])
|
86
89
|
a.run_to_completion
|
87
|
-
|
90
|
+
a.status[1]
|
88
91
|
end
|
89
92
|
end
|
90
93
|
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
|