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.
@@ -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-2011 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -21,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
- # ensure uniqueness of handler to avoid confusion.
33
- raise "#{StdInHandler.name} is already defined" if defined?(StdInHandler)
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
- # Process exit code
75
- attr_reader :exitstatus
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
- @pid = pid
83
- @exitstatus = exitstatus
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