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.
- 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
|