mixlib-shellout 3.2.7-x64-mingw-ucrt

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,431 @@
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) 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_relative "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] && !opts[:password]
36
+ raise InvalidCommandOption, "You must supply a password when supplying a user in windows"
37
+ end
38
+
39
+ if !opts[:user] && opts[:password]
40
+ raise InvalidCommandOption, "You must supply a user when supplying a password in windows"
41
+ end
42
+
43
+ if opts[:elevated] && !opts[:user] && !opts[:password]
44
+ raise InvalidCommandOption, "`elevated` option should be passed only with `username` and `password`."
45
+ end
46
+
47
+ if opts[:elevated] && opts[:elevated] != true && opts[:elevated] != false
48
+ raise InvalidCommandOption, "Invalid value passed for `elevated`. Please provide true/false."
49
+ end
50
+ end
51
+
52
+ #--
53
+ # Missing lots of features from the UNIX version, such as
54
+ # uid, etc.
55
+ def run_command
56
+ #
57
+ # Create pipes to capture stdout and stderr,
58
+ #
59
+ stdout_read, stdout_write = IO.pipe
60
+ stderr_read, stderr_write = IO.pipe
61
+ stdin_read, stdin_write = IO.pipe
62
+ open_streams = [ stdout_read, stderr_read ]
63
+
64
+ begin
65
+
66
+ #
67
+ # Set cwd, environment, appname, etc.
68
+ #
69
+ app_name, command_line = command_to_run(combine_args(*command))
70
+ create_process_args = {
71
+ app_name: app_name,
72
+ command_line: command_line,
73
+ startup_info: {
74
+ stdout: stdout_write,
75
+ stderr: stderr_write,
76
+ stdin: stdin_read,
77
+ },
78
+ environment: inherit_environment.map { |k, v| "#{k}=#{v}" },
79
+ close_handles: false,
80
+ }
81
+ create_process_args[:cwd] = cwd if cwd
82
+ # default to local account database if domain is not specified
83
+ create_process_args[:domain] = domain.nil? ? "." : domain
84
+ create_process_args[:with_logon] = with_logon if with_logon
85
+ create_process_args[:password] = password if password
86
+ create_process_args[:elevated] = elevated if elevated
87
+
88
+ #
89
+ # Start the process
90
+ #
91
+ process, profile, token = Process.create3(create_process_args)
92
+ logger&.debug(format_process(process, app_name, command_line, timeout))
93
+ begin
94
+ # Start pushing data into input
95
+ stdin_write << input if input
96
+
97
+ # Close pipe to kick things off
98
+ stdin_write.close
99
+
100
+ #
101
+ # Wait for the process to finish, consuming output as we go
102
+ #
103
+ start_wait = Time.now
104
+ loop do
105
+ wait_status = WaitForSingleObject(process.process_handle, 0)
106
+ case wait_status
107
+ when WAIT_OBJECT_0
108
+ # Get process exit code
109
+ exit_code = [0].pack("l")
110
+ unless GetExitCodeProcess(process.process_handle, exit_code)
111
+ raise get_last_error
112
+ end
113
+
114
+ @status = ThingThatLooksSortOfLikeAProcessStatus.new
115
+ @status.exitstatus = exit_code.unpack("l").first
116
+
117
+ return self
118
+ when WAIT_TIMEOUT
119
+ # Kill the process
120
+ if (Time.now - start_wait) > timeout
121
+ begin
122
+ require "wmi-lite/wmi"
123
+ wmi = WmiLite::Wmi.new
124
+ kill_process_tree(process.process_id, wmi, logger)
125
+ Process.kill(:KILL, process.process_id)
126
+ rescue SystemCallError
127
+ logger&.warn("Failed to kill timed out process #{process.process_id}")
128
+ end
129
+
130
+ raise Mixlib::ShellOut::CommandTimeout, [
131
+ "command timed out:",
132
+ format_for_exception,
133
+ format_process(process, app_name, command_line, timeout),
134
+ ].join("\n")
135
+ end
136
+
137
+ consume_output(open_streams, stdout_read, stderr_read)
138
+ else
139
+ raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout * 1000}): #{wait_status}"
140
+ end
141
+
142
+ end
143
+
144
+ ensure
145
+ CloseHandle(process.thread_handle) if process.thread_handle
146
+ CloseHandle(process.process_handle) if process.process_handle
147
+ Process.unload_user_profile(token, profile) if profile
148
+ CloseHandle(token) if token
149
+ end
150
+
151
+ ensure
152
+ #
153
+ # Consume all remaining data from the pipes until they are closed
154
+ #
155
+ stdout_write.close
156
+ stderr_write.close
157
+
158
+ while consume_output(open_streams, stdout_read, stderr_read)
159
+ end
160
+ end
161
+ end
162
+
163
+ class ThingThatLooksSortOfLikeAProcessStatus
164
+ attr_accessor :exitstatus
165
+ def success?
166
+ exitstatus == 0
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def consume_output(open_streams, stdout_read, stderr_read)
173
+ return false if open_streams.length == 0
174
+
175
+ ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
176
+ return true unless ready
177
+
178
+ if ready.first.include?(stdout_read)
179
+ begin
180
+ next_chunk = stdout_read.readpartial(READ_SIZE)
181
+ @stdout << next_chunk
182
+ @live_stdout << next_chunk if @live_stdout
183
+ rescue EOFError
184
+ stdout_read.close
185
+ open_streams.delete(stdout_read)
186
+ end
187
+ end
188
+
189
+ if ready.first.include?(stderr_read)
190
+ begin
191
+ next_chunk = stderr_read.readpartial(READ_SIZE)
192
+ @stderr << next_chunk
193
+ @live_stderr << next_chunk if @live_stderr
194
+ rescue EOFError
195
+ stderr_read.close
196
+ open_streams.delete(stderr_read)
197
+ end
198
+ end
199
+
200
+ true
201
+ end
202
+
203
+ # Use to support array passing semantics on windows
204
+ #
205
+ # 1. strings with whitespace or quotes in them need quotes around them.
206
+ # 2. interior quotes need to get backslash escaped (parser needs to know when it really ends).
207
+ # 3. random backlsashes in paths themselves remain untouched.
208
+ # 4. if the argument must be quoted by #1 and terminates in a sequence of backslashes then all the backlashes must themselves
209
+ # be backslash excaped (double the backslashes).
210
+ # 5. if an interior quote that must be escaped by #2 has a sequence of backslashes before it then all the backslashes must
211
+ # themselves be backslash excaped along with the backslash escape of the interior quote (double plus one backslashes).
212
+ #
213
+ # And to restate. We are constructing a string which will be parsed by the windows parser into arguments, and we want those
214
+ # arguments to match the *args array we are passed here. So call the windows parser operation A then we need to apply A^-1 to
215
+ # our args to construct the string so that applying A gives windows back our *args.
216
+ #
217
+ # And when the windows parser sees a series of backslashes followed by a double quote, it has to determine if that double quote
218
+ # is terminating or not, and how many backslashes to insert in the args. So what it does is divide it by two (rounding down) to
219
+ # get the number of backslashes to insert. Then if it is even the double quotes terminate the argument. If it is even the
220
+ # double quotes are interior double quotes (the extra backslash quotes the double quote).
221
+ #
222
+ # We construct the inverse operation so interior double quotes preceeded by N backslashes get 2N+1 backslashes in front of the quote,
223
+ # while trailing N backslashes get 2N backslashes in front of the quote that terminates the argument.
224
+ #
225
+ # see: https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
226
+ #
227
+ # @api private
228
+ # @param args [Array<String>] array of command arguments
229
+ # @return String
230
+ def combine_args(*args)
231
+ return args[0] if args.length == 1
232
+
233
+ args.map do |arg|
234
+ if arg =~ /[ \t\n\v"]/
235
+ arg = arg.gsub(/(\\*)"/, '\1\1\"') # interior quotes with N preceeding backslashes need 2N+1 backslashes
236
+ arg = arg.sub(/(\\+)$/, '\1\1') # trailing N backslashes need to become 2N backslashes
237
+ "\"#{arg}\""
238
+ else
239
+ arg
240
+ end
241
+ end.join(" ")
242
+ end
243
+
244
+ def command_to_run(command)
245
+ return run_under_cmd(command) if should_run_under_cmd?(command)
246
+
247
+ candidate = candidate_executable_for_command(command)
248
+
249
+ if candidate.length == 0
250
+ raise Mixlib::ShellOut::EmptyWindowsCommand, "could not parse script/executable out of command: `#{command}`"
251
+ end
252
+
253
+ # Check if the exe exists directly. Otherwise, search PATH.
254
+ exe = which(candidate)
255
+ if exe_needs_cmd?(exe)
256
+ run_under_cmd(command)
257
+ else
258
+ [ exe, command ]
259
+ end
260
+ end
261
+
262
+ # Batch files MUST use cmd; and if we couldn't find the command we're looking for,
263
+ # we assume it must be a cmd builtin.
264
+ def exe_needs_cmd?(exe)
265
+ !exe || exe =~ /\.bat"?$|\.cmd"?$/i
266
+ end
267
+
268
+ # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
269
+ # https://github.com/chef/mixlib-shellout/pull/2#issuecomment-4837859
270
+ # http://ss64.com/nt/syntax-esc.html
271
+ def run_under_cmd(command)
272
+ [ ENV["COMSPEC"], "cmd /c \"#{command}\"" ]
273
+ end
274
+
275
+ # FIXME: this extracts ARGV[0] but is it correct?
276
+ def candidate_executable_for_command(command)
277
+ if command =~ /^\s*"(.*?)"/ || command =~ /^\s*([^\s]+)/
278
+ # If we have quotes, do an exact match, else pick the first word ignoring the leading spaces
279
+ $1
280
+ else
281
+ ""
282
+ end
283
+ end
284
+
285
+ def inherit_environment
286
+ result = {}
287
+ ENV.each_pair do |k, v|
288
+ result[k] = v
289
+ end
290
+
291
+ environment.each_pair do |k, v|
292
+ if v.nil?
293
+ result.delete(k)
294
+ else
295
+ result[k] = v
296
+ end
297
+ end
298
+ result
299
+ end
300
+
301
+ # api: semi-private
302
+ # If there are special characters parsable by cmd.exe (such as file redirection), then
303
+ # this method should return true.
304
+ #
305
+ # This parser is based on
306
+ # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
307
+ def should_run_under_cmd?(command)
308
+ return true if command =~ /^@/
309
+
310
+ quote = nil
311
+ env = false
312
+ env_first_char = false
313
+
314
+ command.dup.each_char do |c|
315
+ case c
316
+ when "'", '"'
317
+ if !quote
318
+ quote = c
319
+ elsif quote == c
320
+ quote = nil
321
+ end
322
+ next
323
+ when ">", "<", "|", "&", "\n"
324
+ return true unless quote
325
+ when "%"
326
+ return true if env
327
+
328
+ env = env_first_char = true
329
+ next
330
+ else
331
+ next unless env
332
+
333
+ if env_first_char
334
+ env_first_char = false
335
+ (env = false) && next if c !~ /[A-Za-z_]/
336
+ end
337
+ env = false if c !~ /[A-Za-z1-9_]/
338
+ end
339
+ end
340
+ false
341
+ end
342
+
343
+ # FIXME: reduce code duplication with chef/chef
344
+ def which(cmd)
345
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") + [""] : [""]
346
+ # windows always searches '.' first
347
+ exts.each do |ext|
348
+ filename = "#{cmd}#{ext}"
349
+ return filename if File.executable?(filename) && !File.directory?(filename)
350
+ end
351
+ # only search through the path if the Filename does not contain separators
352
+ if File.basename(cmd) == cmd
353
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR)
354
+ paths.each do |path|
355
+ exts.each do |ext|
356
+ filename = File.join(path, "#{cmd}#{ext}")
357
+ return filename if File.executable?(filename) && !File.directory?(filename)
358
+ end
359
+ end
360
+ end
361
+ false
362
+ end
363
+
364
+ def system_required_processes
365
+ [
366
+ "System Idle Process",
367
+ "System",
368
+ "spoolsv.exe",
369
+ "lsass.exe",
370
+ "csrss.exe",
371
+ "smss.exe",
372
+ "svchost.exe",
373
+ ]
374
+ end
375
+
376
+ def unsafe_process?(name, logger)
377
+ return false unless system_required_processes.include? name
378
+
379
+ logger.debug(
380
+ "A request to kill a critical system process - #{name} - was received and skipped."
381
+ )
382
+ true
383
+ end
384
+
385
+ # recursively kills all child processes of given pid
386
+ # calls itself querying for children child procs until
387
+ # none remain. Important that a single WmiLite instance
388
+ # is passed in since each creates its own WMI rpc process
389
+ def kill_process_tree(pid, wmi, logger)
390
+ wmi.query("select * from Win32_Process where ParentProcessID=#{pid}").each do |instance|
391
+ next if unsafe_process?(instance.wmi_ole_object.name, logger)
392
+
393
+ child_pid = instance.wmi_ole_object.processid
394
+ kill_process_tree(child_pid, wmi, logger)
395
+ kill_process(instance, logger)
396
+ end
397
+ end
398
+
399
+ def kill_process(instance, logger)
400
+ child_pid = instance.wmi_ole_object.processid
401
+ logger&.debug([
402
+ "killing child process #{child_pid}::",
403
+ "#{instance.wmi_ole_object.Name} of parent #{pid}",
404
+ ].join)
405
+ Process.kill(:KILL, instance.wmi_ole_object.processid)
406
+ rescue SystemCallError
407
+ logger&.debug([
408
+ "Failed to kill child process #{child_pid}::",
409
+ "#{instance.wmi_ole_object.Name} of parent #{pid}",
410
+ ].join)
411
+ end
412
+
413
+ def format_process(process, app_name, command_line, timeout)
414
+ msg = []
415
+ msg << "ProcessId: #{process.process_id}"
416
+ msg << "app_name: #{app_name}"
417
+ msg << "command_line: #{command_line}"
418
+ msg << "timeout: #{timeout}"
419
+ msg.join("\n")
420
+ end
421
+
422
+ # DEPRECATED do not use
423
+ class Utils
424
+ include Mixlib::ShellOut::Windows
425
+ def self.should_run_under_cmd?(cmd)
426
+ Mixlib::ShellOut::Windows::Utils.new.send(:should_run_under_cmd?, cmd)
427
+ end
428
+ end
429
+ end
430
+ end
431
+ end