mixlib-shellout 2.2.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
- module Mixlib
2
- class ShellOut
3
- VERSION = "2.2.0"
4
- end
5
- end
1
+ module Mixlib
2
+ class ShellOut
3
+ VERSION = "2.2.1"
4
+ end
5
+ end
@@ -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