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