mixlib-shellout 2.2.6-universal-mingw32 → 2.2.7-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.
- checksums.yaml +4 -4
- data/Gemfile +15 -12
- data/LICENSE +201 -201
- data/README.md +87 -54
- data/Rakefile +24 -24
- data/lib/mixlib/shellout.rb +357 -357
- data/lib/mixlib/shellout/exceptions.rb +7 -7
- data/lib/mixlib/shellout/unix.rb +415 -415
- data/lib/mixlib/shellout/version.rb +5 -5
- data/lib/mixlib/shellout/windows.rb +382 -362
- data/lib/mixlib/shellout/windows/core_ext.rb +371 -371
- data/mixlib-shellout-windows.gemspec +8 -8
- data/mixlib-shellout.gemspec +24 -24
- metadata +5 -5
@@ -1,5 +1,5 @@
|
|
1
|
-
module Mixlib
|
2
|
-
class ShellOut
|
3
|
-
VERSION = "2.2.
|
4
|
-
end
|
5
|
-
end
|
1
|
+
module Mixlib
|
2
|
+
class ShellOut
|
3
|
+
VERSION = "2.2.7"
|
4
|
+
end
|
5
|
+
end
|
@@ -1,362 +1,382 @@
|
|
1
|
-
#--
|
2
|
-
# Author:: Daniel DeLeo (<dan@
|
3
|
-
# Author:: John Keiser (<jkeiser@
|
4
|
-
# Author:: Ho-Sheng Hsiao (<hosh@
|
5
|
-
# Copyright:: Copyright (c) 2011
|
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
|
-
# Create pipes to capture stdout and stderr,
|
48
|
-
#
|
49
|
-
stdout_read, stdout_write = IO.pipe
|
50
|
-
stderr_read, stderr_write = IO.pipe
|
51
|
-
stdin_read, stdin_write = IO.pipe
|
52
|
-
open_streams = [ stdout_read, stderr_read ]
|
53
|
-
|
54
|
-
begin
|
55
|
-
|
56
|
-
#
|
57
|
-
# Set cwd, environment, appname, etc.
|
58
|
-
#
|
59
|
-
app_name, command_line = command_to_run(self.command)
|
60
|
-
create_process_args = {
|
61
|
-
:app_name => app_name,
|
62
|
-
:command_line => command_line,
|
63
|
-
:startup_info => {
|
64
|
-
:stdout => stdout_write,
|
65
|
-
:stderr => stderr_write,
|
66
|
-
:stdin => stdin_read
|
67
|
-
},
|
68
|
-
:environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
|
69
|
-
:close_handles => false
|
70
|
-
}
|
71
|
-
create_process_args[:cwd] = cwd if cwd
|
72
|
-
# default to local account database if domain is not specified
|
73
|
-
create_process_args[:domain] = domain.nil? ? "." : domain
|
74
|
-
create_process_args[:with_logon] = with_logon if with_logon
|
75
|
-
create_process_args[:password] = password if password
|
76
|
-
|
77
|
-
#
|
78
|
-
# Start the process
|
79
|
-
#
|
80
|
-
process = Process.create(create_process_args)
|
81
|
-
logger.debug(Utils.format_process(process, app_name, command_line, timeout)) if logger
|
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
|
-
require 'wmi-lite/wmi'
|
111
|
-
wmi = WmiLite::Wmi.new
|
112
|
-
Utils.kill_process_tree(process.process_id, wmi, logger)
|
113
|
-
Process.kill(:KILL, process.process_id)
|
114
|
-
rescue Errno::EIO, SystemCallError
|
115
|
-
logger.warn("Failed to kill timed out process #{process.process_id}") if logger
|
116
|
-
end
|
117
|
-
|
118
|
-
raise Mixlib::ShellOut::CommandTimeout, [
|
119
|
-
"command timed out:",
|
120
|
-
format_for_exception,
|
121
|
-
Utils.format_process(process, app_name, command_line, timeout)
|
122
|
-
].join("\n")
|
123
|
-
end
|
124
|
-
|
125
|
-
consume_output(open_streams, stdout_read, stderr_read)
|
126
|
-
else
|
127
|
-
raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
|
128
|
-
end
|
129
|
-
|
130
|
-
end
|
131
|
-
|
132
|
-
ensure
|
133
|
-
CloseHandle(process.thread_handle) if process.thread_handle
|
134
|
-
CloseHandle(process.process_handle) if process.process_handle
|
135
|
-
end
|
136
|
-
|
137
|
-
ensure
|
138
|
-
#
|
139
|
-
# Consume all remaining data from the pipes until they are closed
|
140
|
-
#
|
141
|
-
stdout_write.close
|
142
|
-
stderr_write.close
|
143
|
-
|
144
|
-
while consume_output(open_streams, stdout_read, stderr_read)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
private
|
150
|
-
|
151
|
-
class ThingThatLooksSortOfLikeAProcessStatus
|
152
|
-
attr_accessor :exitstatus
|
153
|
-
def success?
|
154
|
-
exitstatus == 0
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def consume_output(open_streams, stdout_read, stderr_read)
|
159
|
-
return false if open_streams.length == 0
|
160
|
-
ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
|
161
|
-
return true if ! ready
|
162
|
-
|
163
|
-
if ready.first.include?(stdout_read)
|
164
|
-
begin
|
165
|
-
next_chunk = stdout_read.readpartial(READ_SIZE)
|
166
|
-
@stdout << next_chunk
|
167
|
-
@live_stdout << next_chunk if @live_stdout
|
168
|
-
rescue EOFError
|
169
|
-
stdout_read.close
|
170
|
-
open_streams.delete(stdout_read)
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
if ready.first.include?(stderr_read)
|
175
|
-
begin
|
176
|
-
next_chunk = stderr_read.readpartial(READ_SIZE)
|
177
|
-
@stderr << next_chunk
|
178
|
-
@live_stderr << next_chunk if @live_stderr
|
179
|
-
rescue EOFError
|
180
|
-
stderr_read.close
|
181
|
-
open_streams.delete(stderr_read)
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
return true
|
186
|
-
end
|
187
|
-
|
188
|
-
IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
|
189
|
-
|
190
|
-
def command_to_run(command)
|
191
|
-
return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
|
192
|
-
|
193
|
-
candidate = candidate_executable_for_command(command)
|
194
|
-
|
195
|
-
# Don't do searching for empty commands. Let it fail when it runs.
|
196
|
-
return [ nil, command ] if candidate.length == 0
|
197
|
-
|
198
|
-
# Check if the exe exists directly. Otherwise, search PATH.
|
199
|
-
exe = Utils.find_executable(candidate)
|
200
|
-
exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
|
201
|
-
|
202
|
-
# Batch files MUST use cmd; and if we couldn't find the command we're looking for,
|
203
|
-
# we assume it must be a cmd builtin.
|
204
|
-
if exe.nil? || exe =~ IS_BATCH_FILE
|
205
|
-
_run_under_cmd(command)
|
206
|
-
else
|
207
|
-
_run_directly(command, exe)
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
|
212
|
-
# https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
|
213
|
-
# http://ss64.com/nt/syntax-esc.html
|
214
|
-
def _run_under_cmd(command)
|
215
|
-
[ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
|
216
|
-
end
|
217
|
-
|
218
|
-
def _run_directly(command, exe)
|
219
|
-
[ exe, command ]
|
220
|
-
end
|
221
|
-
|
222
|
-
def unquoted_executable_path(command)
|
223
|
-
command[0,command.index(/\s/) || command.length]
|
224
|
-
end
|
225
|
-
|
226
|
-
def candidate_executable_for_command(command)
|
227
|
-
if command =~ /^\s*"(.*?)"/
|
228
|
-
# If we have quotes, do an exact match
|
229
|
-
$1
|
230
|
-
else
|
231
|
-
# Otherwise check everything up to the first space
|
232
|
-
unquoted_executable_path(command).strip
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def inherit_environment
|
237
|
-
result = {}
|
238
|
-
ENV.each_pair do |k,v|
|
239
|
-
result[k] = v
|
240
|
-
end
|
241
|
-
|
242
|
-
environment.each_pair do |k,v|
|
243
|
-
if v == nil
|
244
|
-
result.delete(k)
|
245
|
-
else
|
246
|
-
result[k] = v
|
247
|
-
end
|
248
|
-
end
|
249
|
-
result
|
250
|
-
end
|
251
|
-
|
252
|
-
module Utils
|
253
|
-
# api: semi-private
|
254
|
-
# If there are special characters parsable by cmd.exe (such as file redirection), then
|
255
|
-
# this method should return true.
|
256
|
-
#
|
257
|
-
# This parser is based on
|
258
|
-
# https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
|
259
|
-
def self.should_run_under_cmd?(command)
|
260
|
-
return true if command =~ /^@/
|
261
|
-
|
262
|
-
quote = nil
|
263
|
-
env = false
|
264
|
-
env_first_char = false
|
265
|
-
|
266
|
-
command.dup.each_char do |c|
|
267
|
-
case c
|
268
|
-
when "'", '"'
|
269
|
-
if (!quote)
|
270
|
-
quote = c
|
271
|
-
elsif quote == c
|
272
|
-
quote = nil
|
273
|
-
end
|
274
|
-
next
|
275
|
-
when '>', '<', '|', '&', "\n"
|
276
|
-
return true unless quote
|
277
|
-
when '%'
|
278
|
-
return true if env
|
279
|
-
env = env_first_char = true
|
280
|
-
next
|
281
|
-
else
|
282
|
-
next unless env
|
283
|
-
if env_first_char
|
284
|
-
env_first_char = false
|
285
|
-
env = false and next if c !~ /[A-Za-z_]/
|
286
|
-
end
|
287
|
-
env = false if c !~ /[A-Za-z1-9_]/
|
288
|
-
end
|
289
|
-
end
|
290
|
-
return false
|
291
|
-
end
|
292
|
-
|
293
|
-
def self.pathext
|
294
|
-
@pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
|
295
|
-
end
|
296
|
-
|
297
|
-
# which() mimicks the Unix which command
|
298
|
-
# FIXME: it is not working
|
299
|
-
def self.which(cmd)
|
300
|
-
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
301
|
-
exe = find_executable("#{path}/#{cmd}")
|
302
|
-
return exe if exe
|
303
|
-
end
|
304
|
-
return nil
|
305
|
-
end
|
306
|
-
|
307
|
-
# Windows has a different notion of what "executable" means
|
308
|
-
# The OS will search through valid the extensions and look
|
309
|
-
# for a binary there.
|
310
|
-
def self.find_executable(path)
|
311
|
-
return path if executable? path
|
312
|
-
|
313
|
-
pathext.each do |ext|
|
314
|
-
exe = "#{path}#{ext}"
|
315
|
-
return exe if executable? exe
|
316
|
-
end
|
317
|
-
return nil
|
318
|
-
end
|
319
|
-
|
320
|
-
def self.executable?(path)
|
321
|
-
File.executable?(path) && !File.directory?(path)
|
322
|
-
end
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
1
|
+
#--
|
2
|
+
# Author:: Daniel DeLeo (<dan@chef.io>)
|
3
|
+
# Author:: John Keiser (<jkeiser@chef.io>)
|
4
|
+
# Author:: Ho-Sheng Hsiao (<hosh@chef.io>)
|
5
|
+
# Copyright:: Copyright (c) 2011-2016 Chef Software, 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
|
+
# Create pipes to capture stdout and stderr,
|
48
|
+
#
|
49
|
+
stdout_read, stdout_write = IO.pipe
|
50
|
+
stderr_read, stderr_write = IO.pipe
|
51
|
+
stdin_read, stdin_write = IO.pipe
|
52
|
+
open_streams = [ stdout_read, stderr_read ]
|
53
|
+
|
54
|
+
begin
|
55
|
+
|
56
|
+
#
|
57
|
+
# Set cwd, environment, appname, etc.
|
58
|
+
#
|
59
|
+
app_name, command_line = command_to_run(self.command)
|
60
|
+
create_process_args = {
|
61
|
+
:app_name => app_name,
|
62
|
+
:command_line => command_line,
|
63
|
+
:startup_info => {
|
64
|
+
:stdout => stdout_write,
|
65
|
+
:stderr => stderr_write,
|
66
|
+
:stdin => stdin_read
|
67
|
+
},
|
68
|
+
:environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
|
69
|
+
:close_handles => false
|
70
|
+
}
|
71
|
+
create_process_args[:cwd] = cwd if cwd
|
72
|
+
# default to local account database if domain is not specified
|
73
|
+
create_process_args[:domain] = domain.nil? ? "." : domain
|
74
|
+
create_process_args[:with_logon] = with_logon if with_logon
|
75
|
+
create_process_args[:password] = password if password
|
76
|
+
|
77
|
+
#
|
78
|
+
# Start the process
|
79
|
+
#
|
80
|
+
process = Process.create(create_process_args)
|
81
|
+
logger.debug(Utils.format_process(process, app_name, command_line, timeout)) if logger
|
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
|
+
require 'wmi-lite/wmi'
|
111
|
+
wmi = WmiLite::Wmi.new
|
112
|
+
Utils.kill_process_tree(process.process_id, wmi, logger)
|
113
|
+
Process.kill(:KILL, process.process_id)
|
114
|
+
rescue Errno::EIO, SystemCallError
|
115
|
+
logger.warn("Failed to kill timed out process #{process.process_id}") if logger
|
116
|
+
end
|
117
|
+
|
118
|
+
raise Mixlib::ShellOut::CommandTimeout, [
|
119
|
+
"command timed out:",
|
120
|
+
format_for_exception,
|
121
|
+
Utils.format_process(process, app_name, command_line, timeout)
|
122
|
+
].join("\n")
|
123
|
+
end
|
124
|
+
|
125
|
+
consume_output(open_streams, stdout_read, stderr_read)
|
126
|
+
else
|
127
|
+
raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
ensure
|
133
|
+
CloseHandle(process.thread_handle) if process.thread_handle
|
134
|
+
CloseHandle(process.process_handle) if process.process_handle
|
135
|
+
end
|
136
|
+
|
137
|
+
ensure
|
138
|
+
#
|
139
|
+
# Consume all remaining data from the pipes until they are closed
|
140
|
+
#
|
141
|
+
stdout_write.close
|
142
|
+
stderr_write.close
|
143
|
+
|
144
|
+
while consume_output(open_streams, stdout_read, stderr_read)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
class ThingThatLooksSortOfLikeAProcessStatus
|
152
|
+
attr_accessor :exitstatus
|
153
|
+
def success?
|
154
|
+
exitstatus == 0
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def consume_output(open_streams, stdout_read, stderr_read)
|
159
|
+
return false if open_streams.length == 0
|
160
|
+
ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
|
161
|
+
return true if ! ready
|
162
|
+
|
163
|
+
if ready.first.include?(stdout_read)
|
164
|
+
begin
|
165
|
+
next_chunk = stdout_read.readpartial(READ_SIZE)
|
166
|
+
@stdout << next_chunk
|
167
|
+
@live_stdout << next_chunk if @live_stdout
|
168
|
+
rescue EOFError
|
169
|
+
stdout_read.close
|
170
|
+
open_streams.delete(stdout_read)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
if ready.first.include?(stderr_read)
|
175
|
+
begin
|
176
|
+
next_chunk = stderr_read.readpartial(READ_SIZE)
|
177
|
+
@stderr << next_chunk
|
178
|
+
@live_stderr << next_chunk if @live_stderr
|
179
|
+
rescue EOFError
|
180
|
+
stderr_read.close
|
181
|
+
open_streams.delete(stderr_read)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
return true
|
186
|
+
end
|
187
|
+
|
188
|
+
IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
|
189
|
+
|
190
|
+
def command_to_run(command)
|
191
|
+
return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
|
192
|
+
|
193
|
+
candidate = candidate_executable_for_command(command)
|
194
|
+
|
195
|
+
# Don't do searching for empty commands. Let it fail when it runs.
|
196
|
+
return [ nil, command ] if candidate.length == 0
|
197
|
+
|
198
|
+
# Check if the exe exists directly. Otherwise, search PATH.
|
199
|
+
exe = Utils.find_executable(candidate)
|
200
|
+
exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
|
201
|
+
|
202
|
+
# Batch files MUST use cmd; and if we couldn't find the command we're looking for,
|
203
|
+
# we assume it must be a cmd builtin.
|
204
|
+
if exe.nil? || exe =~ IS_BATCH_FILE
|
205
|
+
_run_under_cmd(command)
|
206
|
+
else
|
207
|
+
_run_directly(command, exe)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
|
212
|
+
# https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
|
213
|
+
# http://ss64.com/nt/syntax-esc.html
|
214
|
+
def _run_under_cmd(command)
|
215
|
+
[ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
|
216
|
+
end
|
217
|
+
|
218
|
+
def _run_directly(command, exe)
|
219
|
+
[ exe, command ]
|
220
|
+
end
|
221
|
+
|
222
|
+
def unquoted_executable_path(command)
|
223
|
+
command[0,command.index(/\s/) || command.length]
|
224
|
+
end
|
225
|
+
|
226
|
+
def candidate_executable_for_command(command)
|
227
|
+
if command =~ /^\s*"(.*?)"/
|
228
|
+
# If we have quotes, do an exact match
|
229
|
+
$1
|
230
|
+
else
|
231
|
+
# Otherwise check everything up to the first space
|
232
|
+
unquoted_executable_path(command).strip
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def inherit_environment
|
237
|
+
result = {}
|
238
|
+
ENV.each_pair do |k,v|
|
239
|
+
result[k] = v
|
240
|
+
end
|
241
|
+
|
242
|
+
environment.each_pair do |k,v|
|
243
|
+
if v == nil
|
244
|
+
result.delete(k)
|
245
|
+
else
|
246
|
+
result[k] = v
|
247
|
+
end
|
248
|
+
end
|
249
|
+
result
|
250
|
+
end
|
251
|
+
|
252
|
+
module Utils
|
253
|
+
# api: semi-private
|
254
|
+
# If there are special characters parsable by cmd.exe (such as file redirection), then
|
255
|
+
# this method should return true.
|
256
|
+
#
|
257
|
+
# This parser is based on
|
258
|
+
# https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
|
259
|
+
def self.should_run_under_cmd?(command)
|
260
|
+
return true if command =~ /^@/
|
261
|
+
|
262
|
+
quote = nil
|
263
|
+
env = false
|
264
|
+
env_first_char = false
|
265
|
+
|
266
|
+
command.dup.each_char do |c|
|
267
|
+
case c
|
268
|
+
when "'", '"'
|
269
|
+
if (!quote)
|
270
|
+
quote = c
|
271
|
+
elsif quote == c
|
272
|
+
quote = nil
|
273
|
+
end
|
274
|
+
next
|
275
|
+
when '>', '<', '|', '&', "\n"
|
276
|
+
return true unless quote
|
277
|
+
when '%'
|
278
|
+
return true if env
|
279
|
+
env = env_first_char = true
|
280
|
+
next
|
281
|
+
else
|
282
|
+
next unless env
|
283
|
+
if env_first_char
|
284
|
+
env_first_char = false
|
285
|
+
env = false and next if c !~ /[A-Za-z_]/
|
286
|
+
end
|
287
|
+
env = false if c !~ /[A-Za-z1-9_]/
|
288
|
+
end
|
289
|
+
end
|
290
|
+
return false
|
291
|
+
end
|
292
|
+
|
293
|
+
def self.pathext
|
294
|
+
@pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
|
295
|
+
end
|
296
|
+
|
297
|
+
# which() mimicks the Unix which command
|
298
|
+
# FIXME: it is not working
|
299
|
+
def self.which(cmd)
|
300
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
301
|
+
exe = find_executable("#{path}/#{cmd}")
|
302
|
+
return exe if exe
|
303
|
+
end
|
304
|
+
return nil
|
305
|
+
end
|
306
|
+
|
307
|
+
# Windows has a different notion of what "executable" means
|
308
|
+
# The OS will search through valid the extensions and look
|
309
|
+
# for a binary there.
|
310
|
+
def self.find_executable(path)
|
311
|
+
return path if executable? path
|
312
|
+
|
313
|
+
pathext.each do |ext|
|
314
|
+
exe = "#{path}#{ext}"
|
315
|
+
return exe if executable? exe
|
316
|
+
end
|
317
|
+
return nil
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.executable?(path)
|
321
|
+
File.executable?(path) && !File.directory?(path)
|
322
|
+
end
|
323
|
+
|
324
|
+
def self.system_required_processes
|
325
|
+
[
|
326
|
+
'System Idle Process',
|
327
|
+
'System',
|
328
|
+
'spoolsv.exe',
|
329
|
+
'lsass.exe',
|
330
|
+
'csrss.exe',
|
331
|
+
'smss.exe',
|
332
|
+
'svchost.exe'
|
333
|
+
]
|
334
|
+
end
|
335
|
+
|
336
|
+
def self.unsafe_process?(name, logger)
|
337
|
+
return false unless system_required_processes.include? name
|
338
|
+
logger.debug(
|
339
|
+
"A request to kill a critical system process - #{name} - was received and skipped."
|
340
|
+
)
|
341
|
+
true
|
342
|
+
end
|
343
|
+
|
344
|
+
# recursively kills all child processes of given pid
|
345
|
+
# calls itself querying for children child procs until
|
346
|
+
# none remain. Important that a single WmiLite instance
|
347
|
+
# is passed in since each creates its own WMI rpc process
|
348
|
+
def self.kill_process_tree(pid, wmi, logger)
|
349
|
+
wmi.query("select * from Win32_Process where ParentProcessID=#{pid}").each do |instance|
|
350
|
+
next if unsafe_process?(instance.wmi_ole_object.name, logger)
|
351
|
+
child_pid = instance.wmi_ole_object.processid
|
352
|
+
kill_process_tree(child_pid, wmi, logger)
|
353
|
+
kill_process(instance, logger)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def self.kill_process(instance, logger)
|
358
|
+
child_pid = instance.wmi_ole_object.processid
|
359
|
+
logger.debug([
|
360
|
+
"killing child process #{child_pid}::",
|
361
|
+
"#{instance.wmi_ole_object.Name} of parent #{pid}"
|
362
|
+
].join) if logger
|
363
|
+
Process.kill(:KILL, instance.wmi_ole_object.processid)
|
364
|
+
rescue Errno::EIO, SystemCallError
|
365
|
+
logger.debug([
|
366
|
+
"Failed to kill child process #{child_pid}::",
|
367
|
+
"#{instance.wmi_ole_object.Name} of parent #{pid}"
|
368
|
+
].join) if logger
|
369
|
+
end
|
370
|
+
|
371
|
+
def self.format_process(process, app_name, command_line, timeout)
|
372
|
+
msg = []
|
373
|
+
msg << "ProcessId: #{process.process_id}"
|
374
|
+
msg << "app_name: #{app_name}"
|
375
|
+
msg << "command_line: #{command_line}"
|
376
|
+
msg << "timeout: #{timeout}"
|
377
|
+
msg.join("\n")
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end # class
|
381
|
+
end
|
382
|
+
end
|