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