right_popen 1.0.21-x86-mswin32-60 → 1.1.3-x86-mswin32-60
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/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
@@ -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
|
@@ -0,0 +1,306 @@
|
|
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 'win32/process'
|
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
|
+
require ::File.expand_path(::File.join(::File.dirname(__FILE__), 'right_popen_ex'))
|
29
|
+
require ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', '..', 'win32', 'right_popen.so')) # win32 native code
|
30
|
+
|
31
|
+
module RightScale
|
32
|
+
module RightPopen
|
33
|
+
class Process < ProcessBase
|
34
|
+
|
35
|
+
def initialize(options={})
|
36
|
+
super(options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Determines if the process is still running.
|
40
|
+
#
|
41
|
+
# === Return
|
42
|
+
# @return [TrueClass|FalseClass] true if running
|
43
|
+
def alive?
|
44
|
+
raise ProcessError.new('Process not started') unless @pid
|
45
|
+
unless @status
|
46
|
+
# note that ::Process.kill(0, pid) is unreliable from win32-process
|
47
|
+
# gem because it can returns a false positive if called before and
|
48
|
+
# then after process termination.
|
49
|
+
handle = ::Windows::Process::OpenProcess.call(
|
50
|
+
desired_access = ::Windows::Process::PROCESS_ALL_ACCESS,
|
51
|
+
inherit_handle = 0,
|
52
|
+
@pid)
|
53
|
+
alive = false
|
54
|
+
if handle != ::Windows::Handle::INVALID_HANDLE_VALUE
|
55
|
+
begin
|
56
|
+
# immediate check (zero milliseconds) to see if process handle is
|
57
|
+
# signalled (i.e. terminated). the process remains signalled after
|
58
|
+
# termination and can be checked repeatedly in this manner (until
|
59
|
+
# the OS recycles the PID at an unspecified time later).
|
60
|
+
result = ::Windows::Synchronize::WaitForSingleObject.call(
|
61
|
+
handle,
|
62
|
+
milliseconds = 0)
|
63
|
+
alive = result == ::Windows::Synchronize::WAIT_TIMEOUT
|
64
|
+
ensure
|
65
|
+
::Windows::Handle::CloseHandle.call(handle)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
wait_for_exit_status unless alive
|
69
|
+
end
|
70
|
+
@status.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Windows must drain all streams on child death in order to ensure all
|
74
|
+
# output is read. if the child closes only one of the streams there is no
|
75
|
+
# possibility of hanging (Windows will simply read EOF).
|
76
|
+
#
|
77
|
+
# === Return
|
78
|
+
# @return [TrueClass|FalseClass] true if draining all
|
79
|
+
def drain_all_upon_death?
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Array] escalating termination signals for this platform
|
84
|
+
def signals_for_interrupt
|
85
|
+
['INT', 'BRK', 'KILL']
|
86
|
+
end
|
87
|
+
|
88
|
+
# spawns a child process using given command and handler target in a
|
89
|
+
# win32-specific manner.
|
90
|
+
#
|
91
|
+
# must be overridden and override must call super.
|
92
|
+
#
|
93
|
+
# === Parameters
|
94
|
+
# @param [String|Array] cmd as shell command or binary to execute
|
95
|
+
# @param [Object] target that implements all handlers (see TargetProxy)
|
96
|
+
#
|
97
|
+
# === Return
|
98
|
+
# @return [TrueClass] always true
|
99
|
+
def spawn(cmd, target)
|
100
|
+
super(cmd, target)
|
101
|
+
|
102
|
+
# garbage collection has no good effect for spawning a child process in
|
103
|
+
# Windows because forking is not supported and so Ruby objects cannot be
|
104
|
+
# shared with child process (although handles can be shared via some
|
105
|
+
# advanced API programming). the following GC call is only for
|
106
|
+
# compatibility with the Linux implementation.
|
107
|
+
::GC.start
|
108
|
+
|
109
|
+
# merge and format environment strings, if necessary.
|
110
|
+
environment_hash = @options[:environment] || {}
|
111
|
+
environment_strings = ::RightScale::RightPopenEx.merge_environment(environment_hash)
|
112
|
+
|
113
|
+
# resolve command string from array, if necessary.
|
114
|
+
if cmd.kind_of?(::Array)
|
115
|
+
escaped = []
|
116
|
+
cmd.flatten.each_with_index do |token, token_index|
|
117
|
+
token = token.to_s
|
118
|
+
if token_index == 0
|
119
|
+
token = self.class.find_executable_in_path(token)
|
120
|
+
end
|
121
|
+
escaped << self.class.quoted_command_token(token)
|
122
|
+
end
|
123
|
+
cmd = escaped.join(' ')
|
124
|
+
else
|
125
|
+
# resolve first token as an executable using PATH, etc.
|
126
|
+
cmd = cmd.to_s
|
127
|
+
delimiter = (cmd[0..0] == '"') ? '"' : ' '
|
128
|
+
if delimiter_offset = cmd.index(delimiter, 1)
|
129
|
+
token = cmd[0..delimiter_offset].strip
|
130
|
+
remainder = cmd[(delimiter_offset + 1)..-1].to_s.strip
|
131
|
+
else
|
132
|
+
token = cmd
|
133
|
+
remainder = ''
|
134
|
+
end
|
135
|
+
token = self.class.find_executable_in_path(token)
|
136
|
+
token = self.class.quoted_command_token(token)
|
137
|
+
if remainder.empty?
|
138
|
+
cmd = token
|
139
|
+
else
|
140
|
+
cmd = "#{token} #{remainder}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
result = []
|
145
|
+
spawner = lambda do
|
146
|
+
# launch cmd using native win32 implementation.
|
147
|
+
result += ::RightScale::RightPopen.popen4(
|
148
|
+
cmd,
|
149
|
+
mode = 't',
|
150
|
+
show_window = false,
|
151
|
+
asynchronous_output = true,
|
152
|
+
environment_strings)
|
153
|
+
end
|
154
|
+
if @options[:directory]
|
155
|
+
# note that invoking Dir.chdir with a block when already inside a
|
156
|
+
# chdir block is can print an annoying warning to STDERR when paths
|
157
|
+
# differ under circumstances that are hard to define.
|
158
|
+
# case sensitivity? forward vs. backslash?
|
159
|
+
# anyway, do our own comparison to try and avoid this warning.
|
160
|
+
current_directory = ::Dir.pwd.gsub("\\", '/')
|
161
|
+
new_directory = ::File.expand_path(@options[:directory]).gsub("\\", '/')
|
162
|
+
if 0 == current_directory.casecmp(new_directory)
|
163
|
+
spawner.call
|
164
|
+
else
|
165
|
+
::Dir.chdir(@options[:directory]) { spawner.call }
|
166
|
+
end
|
167
|
+
else
|
168
|
+
spawner.call
|
169
|
+
end
|
170
|
+
@stdin, @stdout, @stderr, @pid = result
|
171
|
+
start_timer
|
172
|
+
true
|
173
|
+
rescue
|
174
|
+
# catch-all for failure to spawn process ensuring a non-nil status. the
|
175
|
+
# PID most likely is nil but the exit handler can be invoked for async.
|
176
|
+
safe_close_io
|
177
|
+
@status = ::RightScale::RightPopen::ProcessStatus.new(@pid, 1)
|
178
|
+
raise
|
179
|
+
end
|
180
|
+
|
181
|
+
# blocks waiting for process exit status.
|
182
|
+
#
|
183
|
+
# === Return
|
184
|
+
# @return [ProcessStatus] exit status
|
185
|
+
def wait_for_exit_status
|
186
|
+
raise ProcessError.new('Process not started') unless @pid
|
187
|
+
unless @status
|
188
|
+
exitstatus = 0
|
189
|
+
begin
|
190
|
+
# note that win32-process gem doesn't support the no-hang parameter
|
191
|
+
# and returns exit code instead of status.
|
192
|
+
ignored, exitstatus = ::Process.waitpid2(@pid)
|
193
|
+
rescue Process::Error
|
194
|
+
# process is gone, which means we have no recourse to retrieve the
|
195
|
+
# actual exit code.
|
196
|
+
end
|
197
|
+
|
198
|
+
# an interrupted process can still return zero exit status; if we
|
199
|
+
# interrupted it then don't treat it as successful. simulate the Linux
|
200
|
+
# termination signal behavior here.
|
201
|
+
if interrupted?
|
202
|
+
exitstatus = nil
|
203
|
+
termsig = @last_interrupt
|
204
|
+
end
|
205
|
+
@status = ::RightScale::RightPopen::ProcessStatus.new(@pid, exitstatus, termsig)
|
206
|
+
end
|
207
|
+
@status
|
208
|
+
end
|
209
|
+
|
210
|
+
# Finds the given command name in the PATH. this emulates the 'which'
|
211
|
+
# command from linux (without the terminating newline). Supplies the
|
212
|
+
# executable file extension if missing.
|
213
|
+
#
|
214
|
+
# === Parameters
|
215
|
+
# @param [String] token to be qualified
|
216
|
+
#
|
217
|
+
# === Return
|
218
|
+
# @return [String] path to first matching executable file in PATH or nil
|
219
|
+
def self.find_executable_in_path(token)
|
220
|
+
# strip any surrounding double-quotes (single quotes are considered to
|
221
|
+
# be literals in Windows).
|
222
|
+
token = unquoted_command_token(token)
|
223
|
+
unless token.empty?
|
224
|
+
# note that File.executable? returns a false positive in Windows for
|
225
|
+
# directory paths, so only use File.file?
|
226
|
+
return executable_path(token) if File.file?(token)
|
227
|
+
|
228
|
+
# must search all known (executable) path extensions unless the
|
229
|
+
# explicit extension was given. this handles a case such as 'curl'
|
230
|
+
# which can either be on the path as 'curl.exe' or as a command shell
|
231
|
+
# shortcut called 'curl.cmd', etc.
|
232
|
+
use_path_extensions = 0 == File.extname(token).length
|
233
|
+
path_extensions = use_path_extensions ? (ENV['PATHEXT'] || '').split(/;/) : nil
|
234
|
+
|
235
|
+
# must check the current working directory first just to be completely
|
236
|
+
# sure what would happen if the command were executed. note that Linux
|
237
|
+
# ignores the CWD, so this is platform-specific behavior for Windows.
|
238
|
+
cwd = Dir.getwd
|
239
|
+
path = ENV['PATH']
|
240
|
+
path = (path.nil? || 0 == path.length) ? cwd : (cwd + ';' + path)
|
241
|
+
path.split(/;/).each do |dir|
|
242
|
+
# note that PATH elements are optionally double-quoted.
|
243
|
+
dir = unquoted_command_token(dir)
|
244
|
+
if use_path_extensions
|
245
|
+
path_extensions.each do |path_extension|
|
246
|
+
path = File.join(dir, token + path_extension)
|
247
|
+
return executable_path(path) if File.file?(path)
|
248
|
+
end
|
249
|
+
else
|
250
|
+
path = File.join(dir, token)
|
251
|
+
return executable_path(path) if File.file?(path)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# cannot be resolved.
|
257
|
+
return nil
|
258
|
+
end
|
259
|
+
|
260
|
+
# Determines if the given command token requires double-quotes.
|
261
|
+
#
|
262
|
+
# === Parameter
|
263
|
+
# @param [String] token
|
264
|
+
#
|
265
|
+
# === Return
|
266
|
+
# @return [String] quoted token or unchanged
|
267
|
+
def self.quoted_command_token(token)
|
268
|
+
token = "\"#{token}\"" if token[0..0] != '"' && token.index(' ')
|
269
|
+
token
|
270
|
+
end
|
271
|
+
|
272
|
+
# Determines if the given command token has double-quotes that need to be
|
273
|
+
# removed.
|
274
|
+
#
|
275
|
+
# === Parameter
|
276
|
+
# @param [String] token
|
277
|
+
#
|
278
|
+
# === Return
|
279
|
+
# @return [String] unquoted token or unchanged
|
280
|
+
def self.unquoted_command_token(token)
|
281
|
+
delimiter = '"'
|
282
|
+
if token[0..0] == delimiter
|
283
|
+
delimiter_offset = token.index(delimiter, delimiter.length)
|
284
|
+
if delimiter_offset
|
285
|
+
token = token[1..(delimiter_offset-1)].strip
|
286
|
+
else
|
287
|
+
token = token[1..-1].strip
|
288
|
+
end
|
289
|
+
end
|
290
|
+
token
|
291
|
+
end
|
292
|
+
|
293
|
+
# Makes a pretty path for executing a command in Windows.
|
294
|
+
#
|
295
|
+
# === Parameters
|
296
|
+
# @param [String] path to qualify
|
297
|
+
#
|
298
|
+
# === Return
|
299
|
+
# @return [String] fully qualified executable path
|
300
|
+
def self.executable_path(path)
|
301
|
+
::File.expand_path(path).gsub('/', "\\")
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
#--
|
2
|
-
# Copyright (c) 2009-
|
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,244 +21,20 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
23
|
|
24
|
-
require 'rubygems'
|
25
|
-
require 'eventmachine'
|
26
|
-
require 'win32/process'
|
27
|
-
|
28
|
-
require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'win32', 'right_popen.so')) # win32 native code
|
29
|
-
|
30
24
|
module RightScale
|
31
25
|
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
# Eventmachine callback handler for stdin stream
|
36
|
-
module StdInHandler
|
37
|
-
|
38
|
-
# === Parameters
|
39
|
-
# options[:input](String):: Input to be streamed into child process stdin
|
40
|
-
# stream_in(IO):: Standard input stream.
|
41
|
-
def initialize(options, stream_in)
|
42
|
-
@stream_in = stream_in
|
43
|
-
@input = options[:input]
|
44
|
-
end
|
45
|
-
|
46
|
-
# Eventmachine callback asking for more to write
|
47
|
-
# Send input and close stream in
|
48
|
-
def post_init
|
49
|
-
if @input
|
50
|
-
send_data(@input)
|
51
|
-
close_connection_after_writing
|
52
|
-
@input = nil
|
53
|
-
else
|
54
|
-
close_connection
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
# ensure uniqueness of handler to avoid confusion.
|
61
|
-
raise "#{StdOutHandler.name} is already defined" if defined?(StdOutHandler)
|
62
|
-
|
63
|
-
# Provides an eventmachine callback handler for the stdout stream.
|
64
|
-
module StdOutHandler
|
65
|
-
|
66
|
-
# Quacks like Process::Status, which we cannot instantiate ourselves because
|
67
|
-
# has no public new method. RightScale::popen3 needs this because the
|
68
|
-
# 'win32/process' gem currently won't return Process::Status objects but
|
69
|
-
# only returns a [pid, exitstatus] value.
|
70
|
-
class Status
|
71
|
-
# Process ID
|
72
|
-
attr_reader :pid
|
26
|
+
# helper classes for the win32 implementation of RightPopen.
|
27
|
+
module RightPopenEx
|
73
28
|
|
74
|
-
|
75
|
-
|
29
|
+
# @deprecated because custom status is not specific to win32 platform.
|
30
|
+
class Status < ::RightScale::RightPopen::ProcessStatus
|
76
31
|
|
77
|
-
# === Parameters
|
78
|
-
# pid(Integer):: Process ID.
|
79
|
-
#
|
80
|
-
# exitstatus(Integer):: Process exit code
|
81
32
|
def initialize(pid, exitstatus)
|
82
|
-
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
# Simulates Process::Status.exited?
|
87
|
-
#
|
88
|
-
# === Returns
|
89
|
-
# true in all cases because this object cannot be returned until the
|
90
|
-
# process exits
|
91
|
-
def exited?
|
92
|
-
return true
|
93
|
-
end
|
94
|
-
|
95
|
-
# Simulates Process::Status.success?
|
96
|
-
#
|
97
|
-
# === Returns
|
98
|
-
# true if the process returned zero as its exit code
|
99
|
-
def success?
|
100
|
-
return @exitstatus ? (0 == @exitstatus) : true;
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# === Parameters
|
105
|
-
# options[:target](Object):: Object defining handler methods to be called.
|
106
|
-
# options[:stdout_handler](String):: Token for stdout handler method name.
|
107
|
-
# options[:exit_handler](String):: Token for exit handler method name.
|
108
|
-
# stderr_eventable(Connector):: EM object representing stderr handler.
|
109
|
-
# stream_out(IO):: Standard output stream.
|
110
|
-
# pid(Integer):: Child process ID.
|
111
|
-
def initialize(options, stderr_eventable, stream_out, pid)
|
112
|
-
@target = options[:target]
|
113
|
-
@stdout_handler = options[:stdout_handler]
|
114
|
-
@exit_handler = options[:exit_handler]
|
115
|
-
@stderr_eventable = stderr_eventable
|
116
|
-
@stream_out = stream_out
|
117
|
-
@pid = pid
|
118
|
-
@status = nil
|
119
|
-
end
|
120
|
-
|
121
|
-
# Callback from EM to asynchronously read the stdout stream. Note that this
|
122
|
-
# callback mechanism is deprecated after EM v0.12.8
|
123
|
-
def notify_readable
|
124
|
-
data = RightPopen.async_read(@stream_out)
|
125
|
-
receive_data(data) if (data && data.length > 0)
|
126
|
-
detach unless data
|
127
|
-
end
|
128
|
-
|
129
|
-
# Callback from EM to receive data, which we also use to handle the
|
130
|
-
# asynchronous data we read ourselves.
|
131
|
-
def receive_data(data)
|
132
|
-
@target.method(@stdout_handler).call(data) if @stdout_handler
|
133
|
-
end
|
134
|
-
|
135
|
-
# Override of Connection.get_status() for Windows implementation.
|
136
|
-
def get_status
|
137
|
-
unless @status
|
138
|
-
begin
|
139
|
-
@status = Status.new(@pid, Process.waitpid2(@pid)[1])
|
140
|
-
rescue Process::Error
|
141
|
-
# process is gone, which means we have no recourse to retrieve the
|
142
|
-
# actual exit code; let's be optimistic.
|
143
|
-
@status = Status.new(@pid, 0)
|
144
|
-
end
|
33
|
+
super(pid, exitstatus)
|
34
|
+
warn "WARNING: RightScale::RightPopenEx::Status is deprecated in favor of ::RightScale::RightPopen::ProcessStatus"
|
145
35
|
end
|
146
|
-
return @status
|
147
|
-
end
|
148
|
-
|
149
|
-
# Callback from EM to unbind.
|
150
|
-
def unbind
|
151
|
-
# We force the stderr watched handler to go away so that
|
152
|
-
# we don't end up with a broken pipe
|
153
|
-
@stderr_eventable.force_detach if @stderr_eventable
|
154
|
-
@target.method(@exit_handler).call(get_status) if @exit_handler
|
155
|
-
@stream_out.close
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
# ensure uniqueness of handler to avoid confusion.
|
160
|
-
raise "#{StdErrHandler.name} is already defined" if defined?(StdErrHandler)
|
161
|
-
|
162
|
-
# Provides an eventmachine callback handler for the stderr stream.
|
163
|
-
module StdErrHandler
|
164
|
-
|
165
|
-
# === Parameters
|
166
|
-
# options[:target](Object):: Object defining handler methods to be called.
|
167
|
-
# options[:stderr_handler](Symbol):: Token for stderr handler method name.
|
168
|
-
# stream_err(IO):: Standard error stream.
|
169
|
-
def initialize(options, stream_err)
|
170
|
-
@target = options[:target]
|
171
|
-
@stderr_handler = options[:stderr_handler]
|
172
|
-
@stream_err = stream_err
|
173
|
-
@unbound = false
|
174
|
-
end
|
175
|
-
|
176
|
-
# Callback from EM to asynchronously read the stderr stream. Note that this
|
177
|
-
# callback mechanism is deprecated after EM v0.12.8
|
178
|
-
def notify_readable
|
179
|
-
data = RightPopen.async_read(@stream_err)
|
180
|
-
receive_data(data) if (data && data.length > 0)
|
181
|
-
detach unless data
|
182
|
-
end
|
183
|
-
|
184
|
-
# Callback from EM to receive data, which we also use to handle the
|
185
|
-
# asynchronous data we read ourselves.
|
186
|
-
def receive_data(data)
|
187
|
-
@target.method(@stderr_handler).call(data)
|
188
|
-
end
|
189
|
-
|
190
|
-
# Callback from EM to unbind.
|
191
|
-
def unbind
|
192
|
-
@unbound = true
|
193
|
-
@stream_err.close
|
194
36
|
end
|
195
37
|
|
196
|
-
# Forces detachment of the stderr handler on EM's next tick.
|
197
|
-
def force_detach
|
198
|
-
# Use next tick to prevent issue in EM where descriptors list
|
199
|
-
# gets out-of-sync when calling detach in an unbind callback
|
200
|
-
EM.next_tick { detach unless @unbound }
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
# Creates a child process and connects event handlers to the standard output
|
205
|
-
# and error streams used by the created process. Connectors use named pipes
|
206
|
-
# and asynchronous I/O in the native Windows implementation.
|
207
|
-
#
|
208
|
-
# See RightScale.popen3
|
209
|
-
def self.popen3_imp(options, &block)
|
210
|
-
raise "EventMachine reactor must be started" unless EM.reactor_running?
|
211
|
-
|
212
|
-
# merge command string
|
213
|
-
unless options[:command].instance_of?(String)
|
214
|
-
options[:command] = options[:command].join(' ')
|
215
|
-
end
|
216
|
-
|
217
|
-
# merge and format environment strings, if necessary.
|
218
|
-
environment_hash = options[:environment] || {}
|
219
|
-
environment_strings = RightPopenEx.merge_environment(environment_hash)
|
220
|
-
|
221
|
-
# resolve command string from array, if necessary.
|
222
|
-
cmd = options[:command]
|
223
|
-
if cmd.kind_of?(Array)
|
224
|
-
escaped = []
|
225
|
-
cmd.flatten.each do |arg|
|
226
|
-
value = arg.to_s
|
227
|
-
escaped << (value.index(' ') ? "\"#{value}\"" : value)
|
228
|
-
end
|
229
|
-
cmd = escaped.join(" ")
|
230
|
-
end
|
231
|
-
|
232
|
-
# launch cmd and request asynchronous output.
|
233
|
-
mode = "t"
|
234
|
-
show_window = false
|
235
|
-
asynchronous_output = true
|
236
|
-
stream_in, stream_out, stream_err, pid = RightPopen.popen4(cmd, mode, show_window, asynchronous_output, environment_strings)
|
237
|
-
|
238
|
-
# close input immediately.
|
239
|
-
stream_in.close if options[:input].nil?
|
240
|
-
|
241
|
-
# attach handlers to event machine and let it monitor incoming data. the
|
242
|
-
# streams aren't used directly by the connectors except that they are closed
|
243
|
-
# on unbind.
|
244
|
-
stderr_eventable = EM.watch(stream_err, StdErrHandler, options, stream_err) { |c| c.notify_readable = true } if options[:stderr_handler]
|
245
|
-
EM.watch(stream_out, StdOutHandler, options, stderr_eventable, stream_out, pid) do |c|
|
246
|
-
c.notify_readable = true
|
247
|
-
options[:target].method(options[:pid_handler]).call(pid) if options[:pid_handler]
|
248
|
-
end
|
249
|
-
EM.attach(stream_in, StdInHandler, options, stream_in) if options[:input]
|
250
|
-
|
251
|
-
# note that control returns to the caller, but the launched cmd continues
|
252
|
-
# running and sends output to the handlers. the caller is not responsible
|
253
|
-
# for waiting for the process to terminate or closing streams as the
|
254
|
-
# watched eventables will handle this automagically. notification will be
|
255
|
-
# sent to the exit_handler on process termination.
|
256
|
-
true
|
257
|
-
end
|
258
|
-
|
259
|
-
protected
|
260
|
-
|
261
|
-
module RightPopenEx
|
262
38
|
# Key class for case-insensitive hash insertion/lookup.
|
263
39
|
class NoCaseKey
|
264
40
|
# Internal key
|
@@ -490,4 +266,5 @@ module RightScale
|
|
490
266
|
end
|
491
267
|
|
492
268
|
end
|
269
|
+
|
493
270
|
end
|