mixlib-shellout 2.2.0-universal-mingw32 → 2.2.1-universal-mingw32

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.
@@ -1,315 +1,319 @@
1
- #--
2
- # Author:: Daniel DeLeo (<dan@opscode.com>)
3
- # Author:: John Keiser (<jkeiser@opscode.com>)
4
- # Author:: Ho-Sheng Hsiao (<hosh@opscode.com>)
5
- # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc.
6
- # License:: Apache License, Version 2.0
7
- #
8
- # Licensed under the Apache License, Version 2.0 (the "License");
9
- # you may not use this file except in compliance with the License.
10
- # You may obtain a copy of the License at
11
- #
12
- # http://www.apache.org/licenses/LICENSE-2.0
13
- #
14
- # Unless required by applicable law or agreed to in writing, software
15
- # distributed under the License is distributed on an "AS IS" BASIS,
16
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- # See the License for the specific language governing permissions and
18
- # limitations under the License.
19
- #
20
-
21
- require 'win32/process'
22
- require 'mixlib/shellout/windows/core_ext'
23
-
24
- module Mixlib
25
- class ShellOut
26
- module Windows
27
-
28
- include Process::Functions
29
- include Process::Constants
30
-
31
- TIME_SLICE = 0.05
32
-
33
- # Option validation that is windows specific
34
- def validate_options(opts)
35
- if opts[:user]
36
- unless opts[:password]
37
- raise InvalidCommandOption, "You must supply both a username and password when supplying a user in windows"
38
- end
39
- end
40
- end
41
-
42
- #--
43
- # Missing lots of features from the UNIX version, such as
44
- # uid, etc.
45
- def run_command
46
-
47
- #
48
- # Create pipes to capture stdout and stderr,
49
- #
50
- stdout_read, stdout_write = IO.pipe
51
- stderr_read, stderr_write = IO.pipe
52
- stdin_read, stdin_write = IO.pipe
53
- open_streams = [ stdout_read, stderr_read ]
54
-
55
- begin
56
-
57
- #
58
- # Set cwd, environment, appname, etc.
59
- #
60
- app_name, command_line = command_to_run(self.command)
61
- create_process_args = {
62
- :app_name => app_name,
63
- :command_line => command_line,
64
- :startup_info => {
65
- :stdout => stdout_write,
66
- :stderr => stderr_write,
67
- :stdin => stdin_read
68
- },
69
- :environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
70
- :close_handles => false
71
- }
72
- create_process_args[:cwd] = cwd if cwd
73
- # default to local account database if domain is not specified
74
- create_process_args[:domain] = domain.nil? ? "." : domain
75
- create_process_args[:with_logon] = with_logon if with_logon
76
- create_process_args[:password] = password if password
77
-
78
- #
79
- # Start the process
80
- #
81
- process = Process.create(create_process_args)
82
- begin
83
- # Start pushing data into input
84
- stdin_write << input if input
85
-
86
- # Close pipe to kick things off
87
- stdin_write.close
88
-
89
- #
90
- # Wait for the process to finish, consuming output as we go
91
- #
92
- start_wait = Time.now
93
- while true
94
- wait_status = WaitForSingleObject(process.process_handle, 0)
95
- case wait_status
96
- when WAIT_OBJECT_0
97
- # Get process exit code
98
- exit_code = [0].pack('l')
99
- unless GetExitCodeProcess(process.process_handle, exit_code)
100
- raise get_last_error
101
- end
102
- @status = ThingThatLooksSortOfLikeAProcessStatus.new
103
- @status.exitstatus = exit_code.unpack('l').first
104
-
105
- return self
106
- when WAIT_TIMEOUT
107
- # Kill the process
108
- if (Time.now - start_wait) > timeout
109
- begin
110
- Process.kill(:KILL, process.process_id)
111
- rescue Errno::EIO
112
- logger.warn("Failed to kill timed out process #{process.process_id}") if logger
113
- end
114
-
115
- raise Mixlib::ShellOut::CommandTimeout, "command timed out:\n#{format_for_exception}"
116
- end
117
-
118
- consume_output(open_streams, stdout_read, stderr_read)
119
- else
120
- raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
121
- end
122
-
123
- end
124
-
125
- ensure
126
- CloseHandle(process.thread_handle) if process.thread_handle
127
- CloseHandle(process.process_handle) if process.process_handle
128
- end
129
-
130
- ensure
131
- #
132
- # Consume all remaining data from the pipes until they are closed
133
- #
134
- stdout_write.close
135
- stderr_write.close
136
-
137
- while consume_output(open_streams, stdout_read, stderr_read)
138
- end
139
- end
140
- end
141
-
142
- private
143
-
144
- class ThingThatLooksSortOfLikeAProcessStatus
145
- attr_accessor :exitstatus
146
- def success?
147
- exitstatus == 0
148
- end
149
- end
150
-
151
- def consume_output(open_streams, stdout_read, stderr_read)
152
- return false if open_streams.length == 0
153
- ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
154
- return true if ! ready
155
-
156
- if ready.first.include?(stdout_read)
157
- begin
158
- next_chunk = stdout_read.readpartial(READ_SIZE)
159
- @stdout << next_chunk
160
- @live_stdout << next_chunk if @live_stdout
161
- rescue EOFError
162
- stdout_read.close
163
- open_streams.delete(stdout_read)
164
- end
165
- end
166
-
167
- if ready.first.include?(stderr_read)
168
- begin
169
- next_chunk = stderr_read.readpartial(READ_SIZE)
170
- @stderr << next_chunk
171
- @live_stderr << next_chunk if @live_stderr
172
- rescue EOFError
173
- stderr_read.close
174
- open_streams.delete(stderr_read)
175
- end
176
- end
177
-
178
- return true
179
- end
180
-
181
- IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
182
-
183
- def command_to_run(command)
184
- return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
185
-
186
- candidate = candidate_executable_for_command(command)
187
-
188
- # Don't do searching for empty commands. Let it fail when it runs.
189
- return [ nil, command ] if candidate.length == 0
190
-
191
- # Check if the exe exists directly. Otherwise, search PATH.
192
- exe = Utils.find_executable(candidate)
193
- exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
194
-
195
- # Batch files MUST use cmd; and if we couldn't find the command we're looking for,
196
- # we assume it must be a cmd builtin.
197
- if exe.nil? || exe =~ IS_BATCH_FILE
198
- _run_under_cmd(command)
199
- else
200
- _run_directly(command, exe)
201
- end
202
- end
203
-
204
- # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
205
- # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
206
- # http://ss64.com/nt/syntax-esc.html
207
- def _run_under_cmd(command)
208
- [ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
209
- end
210
-
211
- def _run_directly(command, exe)
212
- [ exe, command ]
213
- end
214
-
215
- def unquoted_executable_path(command)
216
- command[0,command.index(/\s/) || command.length]
217
- end
218
-
219
- def candidate_executable_for_command(command)
220
- if command =~ /^\s*"(.*?)"/
221
- # If we have quotes, do an exact match
222
- $1
223
- else
224
- # Otherwise check everything up to the first space
225
- unquoted_executable_path(command).strip
226
- end
227
- end
228
-
229
- def inherit_environment
230
- result = {}
231
- ENV.each_pair do |k,v|
232
- result[k] = v
233
- end
234
-
235
- environment.each_pair do |k,v|
236
- if v == nil
237
- result.delete(k)
238
- else
239
- result[k] = v
240
- end
241
- end
242
- result
243
- end
244
-
245
- module Utils
246
- # api: semi-private
247
- # If there are special characters parsable by cmd.exe (such as file redirection), then
248
- # this method should return true.
249
- #
250
- # This parser is based on
251
- # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
252
- def self.should_run_under_cmd?(command)
253
- return true if command =~ /^@/
254
-
255
- quote = nil
256
- env = false
257
- env_first_char = false
258
-
259
- command.dup.each_char do |c|
260
- case c
261
- when "'", '"'
262
- if (!quote)
263
- quote = c
264
- elsif quote == c
265
- quote = nil
266
- end
267
- next
268
- when '>', '<', '|', '&', "\n"
269
- return true unless quote
270
- when '%'
271
- return true if env
272
- env = env_first_char = true
273
- next
274
- else
275
- next unless env
276
- if env_first_char
277
- env_first_char = false
278
- env = false and next if c !~ /[A-Za-z_]/
279
- end
280
- env = false if c !~ /[A-Za-z1-9_]/
281
- end
282
- end
283
- return false
284
- end
285
-
286
- def self.pathext
287
- @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
288
- end
289
-
290
- # which() mimicks the Unix which command
291
- # FIXME: it is not working
292
- def self.which(cmd)
293
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
294
- exe = find_executable("#{path}/#{cmd}")
295
- return exe if exe
296
- end
297
- return nil
298
- end
299
-
300
- # Windows has a different notion of what "executable" means
301
- # The OS will search through valid the extensions and look
302
- # for a binary there.
303
- def self.find_executable(path)
304
- return path if File.executable? path
305
-
306
- pathext.each do |ext|
307
- exe = "#{path}#{ext}"
308
- return exe if File.executable? exe
309
- end
310
- return nil
311
- end
312
- end
313
- end # class
314
- end
315
- end
1
+ #--
2
+ # Author:: Daniel DeLeo (<dan@opscode.com>)
3
+ # Author:: John Keiser (<jkeiser@opscode.com>)
4
+ # Author:: Ho-Sheng Hsiao (<hosh@opscode.com>)
5
+ # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc.
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+
21
+ require 'win32/process'
22
+ require 'mixlib/shellout/windows/core_ext'
23
+
24
+ module Mixlib
25
+ class ShellOut
26
+ module Windows
27
+
28
+ include Process::Functions
29
+ include Process::Constants
30
+
31
+ TIME_SLICE = 0.05
32
+
33
+ # Option validation that is windows specific
34
+ def validate_options(opts)
35
+ if opts[:user]
36
+ unless opts[:password]
37
+ raise InvalidCommandOption, "You must supply both a username and password when supplying a user in windows"
38
+ end
39
+ end
40
+ end
41
+
42
+ #--
43
+ # Missing lots of features from the UNIX version, such as
44
+ # uid, etc.
45
+ def run_command
46
+
47
+ #
48
+ # Create pipes to capture stdout and stderr,
49
+ #
50
+ stdout_read, stdout_write = IO.pipe
51
+ stderr_read, stderr_write = IO.pipe
52
+ stdin_read, stdin_write = IO.pipe
53
+ open_streams = [ stdout_read, stderr_read ]
54
+
55
+ begin
56
+
57
+ #
58
+ # Set cwd, environment, appname, etc.
59
+ #
60
+ app_name, command_line = command_to_run(self.command)
61
+ create_process_args = {
62
+ :app_name => app_name,
63
+ :command_line => command_line,
64
+ :startup_info => {
65
+ :stdout => stdout_write,
66
+ :stderr => stderr_write,
67
+ :stdin => stdin_read
68
+ },
69
+ :environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
70
+ :close_handles => false
71
+ }
72
+ create_process_args[:cwd] = cwd if cwd
73
+ # default to local account database if domain is not specified
74
+ create_process_args[:domain] = domain.nil? ? "." : domain
75
+ create_process_args[:with_logon] = with_logon if with_logon
76
+ create_process_args[:password] = password if password
77
+
78
+ #
79
+ # Start the process
80
+ #
81
+ process = Process.create(create_process_args)
82
+ begin
83
+ # Start pushing data into input
84
+ stdin_write << input if input
85
+
86
+ # Close pipe to kick things off
87
+ stdin_write.close
88
+
89
+ #
90
+ # Wait for the process to finish, consuming output as we go
91
+ #
92
+ start_wait = Time.now
93
+ while true
94
+ wait_status = WaitForSingleObject(process.process_handle, 0)
95
+ case wait_status
96
+ when WAIT_OBJECT_0
97
+ # Get process exit code
98
+ exit_code = [0].pack('l')
99
+ unless GetExitCodeProcess(process.process_handle, exit_code)
100
+ raise get_last_error
101
+ end
102
+ @status = ThingThatLooksSortOfLikeAProcessStatus.new
103
+ @status.exitstatus = exit_code.unpack('l').first
104
+
105
+ return self
106
+ when WAIT_TIMEOUT
107
+ # Kill the process
108
+ if (Time.now - start_wait) > timeout
109
+ begin
110
+ Process.kill(:KILL, process.process_id)
111
+ rescue Errno::EIO
112
+ logger.warn("Failed to kill timed out process #{process.process_id}") if logger
113
+ end
114
+
115
+ raise Mixlib::ShellOut::CommandTimeout, "command timed out:\n#{format_for_exception}"
116
+ end
117
+
118
+ consume_output(open_streams, stdout_read, stderr_read)
119
+ else
120
+ raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
121
+ end
122
+
123
+ end
124
+
125
+ ensure
126
+ CloseHandle(process.thread_handle) if process.thread_handle
127
+ CloseHandle(process.process_handle) if process.process_handle
128
+ end
129
+
130
+ ensure
131
+ #
132
+ # Consume all remaining data from the pipes until they are closed
133
+ #
134
+ stdout_write.close
135
+ stderr_write.close
136
+
137
+ while consume_output(open_streams, stdout_read, stderr_read)
138
+ end
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ class ThingThatLooksSortOfLikeAProcessStatus
145
+ attr_accessor :exitstatus
146
+ def success?
147
+ exitstatus == 0
148
+ end
149
+ end
150
+
151
+ def consume_output(open_streams, stdout_read, stderr_read)
152
+ return false if open_streams.length == 0
153
+ ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
154
+ return true if ! ready
155
+
156
+ if ready.first.include?(stdout_read)
157
+ begin
158
+ next_chunk = stdout_read.readpartial(READ_SIZE)
159
+ @stdout << next_chunk
160
+ @live_stdout << next_chunk if @live_stdout
161
+ rescue EOFError
162
+ stdout_read.close
163
+ open_streams.delete(stdout_read)
164
+ end
165
+ end
166
+
167
+ if ready.first.include?(stderr_read)
168
+ begin
169
+ next_chunk = stderr_read.readpartial(READ_SIZE)
170
+ @stderr << next_chunk
171
+ @live_stderr << next_chunk if @live_stderr
172
+ rescue EOFError
173
+ stderr_read.close
174
+ open_streams.delete(stderr_read)
175
+ end
176
+ end
177
+
178
+ return true
179
+ end
180
+
181
+ IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
182
+
183
+ def command_to_run(command)
184
+ return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
185
+
186
+ candidate = candidate_executable_for_command(command)
187
+
188
+ # Don't do searching for empty commands. Let it fail when it runs.
189
+ return [ nil, command ] if candidate.length == 0
190
+
191
+ # Check if the exe exists directly. Otherwise, search PATH.
192
+ exe = Utils.find_executable(candidate)
193
+ exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
194
+
195
+ # Batch files MUST use cmd; and if we couldn't find the command we're looking for,
196
+ # we assume it must be a cmd builtin.
197
+ if exe.nil? || exe =~ IS_BATCH_FILE
198
+ _run_under_cmd(command)
199
+ else
200
+ _run_directly(command, exe)
201
+ end
202
+ end
203
+
204
+ # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
205
+ # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
206
+ # http://ss64.com/nt/syntax-esc.html
207
+ def _run_under_cmd(command)
208
+ [ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
209
+ end
210
+
211
+ def _run_directly(command, exe)
212
+ [ exe, command ]
213
+ end
214
+
215
+ def unquoted_executable_path(command)
216
+ command[0,command.index(/\s/) || command.length]
217
+ end
218
+
219
+ def candidate_executable_for_command(command)
220
+ if command =~ /^\s*"(.*?)"/
221
+ # If we have quotes, do an exact match
222
+ $1
223
+ else
224
+ # Otherwise check everything up to the first space
225
+ unquoted_executable_path(command).strip
226
+ end
227
+ end
228
+
229
+ def inherit_environment
230
+ result = {}
231
+ ENV.each_pair do |k,v|
232
+ result[k] = v
233
+ end
234
+
235
+ environment.each_pair do |k,v|
236
+ if v == nil
237
+ result.delete(k)
238
+ else
239
+ result[k] = v
240
+ end
241
+ end
242
+ result
243
+ end
244
+
245
+ module Utils
246
+ # api: semi-private
247
+ # If there are special characters parsable by cmd.exe (such as file redirection), then
248
+ # this method should return true.
249
+ #
250
+ # This parser is based on
251
+ # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
252
+ def self.should_run_under_cmd?(command)
253
+ return true if command =~ /^@/
254
+
255
+ quote = nil
256
+ env = false
257
+ env_first_char = false
258
+
259
+ command.dup.each_char do |c|
260
+ case c
261
+ when "'", '"'
262
+ if (!quote)
263
+ quote = c
264
+ elsif quote == c
265
+ quote = nil
266
+ end
267
+ next
268
+ when '>', '<', '|', '&', "\n"
269
+ return true unless quote
270
+ when '%'
271
+ return true if env
272
+ env = env_first_char = true
273
+ next
274
+ else
275
+ next unless env
276
+ if env_first_char
277
+ env_first_char = false
278
+ env = false and next if c !~ /[A-Za-z_]/
279
+ end
280
+ env = false if c !~ /[A-Za-z1-9_]/
281
+ end
282
+ end
283
+ return false
284
+ end
285
+
286
+ def self.pathext
287
+ @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
288
+ end
289
+
290
+ # which() mimicks the Unix which command
291
+ # FIXME: it is not working
292
+ def self.which(cmd)
293
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
294
+ exe = find_executable("#{path}/#{cmd}")
295
+ return exe if exe
296
+ end
297
+ return nil
298
+ end
299
+
300
+ # Windows has a different notion of what "executable" means
301
+ # The OS will search through valid the extensions and look
302
+ # for a binary there.
303
+ def self.find_executable(path)
304
+ return path if executable? path
305
+
306
+ pathext.each do |ext|
307
+ exe = "#{path}#{ext}"
308
+ return exe if executable? exe
309
+ end
310
+ return nil
311
+ end
312
+
313
+ def self.executable?(path)
314
+ File.executable?(path) && !File.directory?(path)
315
+ end
316
+ end
317
+ end # class
318
+ end
319
+ end