From 1033f0ba6aab441ffe13ffeb36b3c28881266344 Mon Sep 17 00:00:00 2001 From: Seth Chisamore Date: Thu, 15 Dec 2011 19:32:15 -0500 Subject: [PATCH] [CHEF-2819] fixes for Chef::ShellOut::Windows in 0.10.6 * smart resolution of any file with extension in %PATHEXT% * ensure *.bat and *.cmd files are executed under `cmd /c` * ensure STDIN is *also* redirected as many programs (ie xcopy) will fail silently if only STDOUT is --- chef/lib/chef/shell_out/windows.rb | 85 ++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/chef/lib/chef/shell_out/windows.rb b/chef/lib/chef/shell_out/windows.rb index 46d8ab79cfd..80d6b6fc5bc 100644 --- a/chef/lib/chef/shell_out/windows.rb +++ b/chef/lib/chef/shell_out/windows.rb @@ -42,6 +42,7 @@ def run_command # stdout_read, stdout_write = IO.pipe stderr_read, stderr_write = IO.pipe + stdin_read, stdin_write = IO.pipe open_streams = [ stdout_read, stderr_read ] begin @@ -55,7 +56,8 @@ def run_command :command_line => command_line, :startup_info => { :stdout => stdout_write, - :stderr => stderr_write + :stderr => stderr_write, + :stdin => stdin_read }, :environment => inherit_environment.map { |k,v| "#{k}=#{v}" }, :close_handles => false @@ -152,13 +154,33 @@ def consume_output(open_streams, stdout_read, stderr_read) return true end - SHOULD_USE_CMD = /['"<>|&%]|\b(?:assoc|break|call|cd|chcp|chdir|cls|color|copy|ctty|date|del|dir|echo|endlocal|erase|exit|for|ftype|goto|if|lfnfor|lh|lock|md|mkdir|move|path|pause|popd|prompt|pushd|rd|rem|ren|rename|rmdir|set|setlocal|shift|start|time|title|truename|type|unlock|ver|verify|vol)\b/ + IS_BATCH_FILE = /\.bat|\.cmd$/i def command_to_run - if command =~ SHOULD_USE_CMD + if command =~ /^\s*"(.*)"/ + # If we have quotes, do an exact match + candidate = $1 + else + # Otherwise check everything up to the first space + candidate = command[0,command.index(/\s/) || command.length].strip + end + + # Don't do searching for empty commands. Let it fail when it runs. + if candidate.length == 0 + return [ nil, command ] + end + + # Check if the exe exists directly. Otherwise, search PATH. + exe = find_exe_at_location(candidate) + if exe.nil? && exe !~ /[\\\/]/ + exe = which(command[0,command.index(/\s/) || command.length]) + end + + if exe.nil? || exe =~ IS_BATCH_FILE + # Batch files MUST use cmd; and if we couldn't find the command we're looking for, we assume it must be a cmd builtin. [ ENV['COMSPEC'], "cmd /c #{command}" ] else - [ which(command[0,command.index(/\s/) || command.length]), command ] + [ exe, command ] end end @@ -178,14 +200,23 @@ def inherit_environment result end + def pathext + @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] + end + def which(cmd) - return cmd if File.executable? cmd - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| - exts.each { |ext| - exe = "#{path}/#{cmd}#{ext}" - return exe if File.executable? exe - } + exe = find_exe_at_location("#{path}/${cmd}") + return exe if exe + end + return nil + end + + def find_exe_at_location(path) + return path if File.executable? path + pathext.each do |ext| + exe = "#{path}#{ext}" + return exe if File.executable? exe end return nil end @@ -213,7 +244,7 @@ def create(args) unless args.kind_of?(Hash) raise TypeError, 'Expecting hash-style keyword arguments' end - + valid_keys = %w/ app_name command_line inherit creation_flags cwd environment startup_info thread_inherit process_inherit close_handles with_logon @@ -231,8 +262,8 @@ def create(args) 'creation_flags' => 0, 'close_handles' => true } - - # Validate the keys, and convert symbols and case to lowercase strings. + + # Validate the keys, and convert symbols and case to lowercase strings. args.each{ |key, val| key = key.to_s.downcase unless valid_keys.include?(key) @@ -240,9 +271,9 @@ def create(args) end hash[key] = val } - + si_hash = {} - + # If the startup_info key is present, validate its subkeys if hash['startup_info'] hash['startup_info'].each{ |key, val| @@ -264,7 +295,7 @@ def create(args) raise ArgumentError, 'command_line or app_name must be specified' end end - + # The environment string should be passed as an array of A=B paths, or # as a string of ';' separated paths. if hash['environment'] @@ -312,7 +343,7 @@ def create(args) else handle = get_osfhandle(si_hash[io]) end - + if handle == INVALID_HANDLE_VALUE raise Error, get_last_error end @@ -326,14 +357,14 @@ def create(args) ) raise Error, get_last_error unless bool - + si_hash[io] = handle si_hash['startf_flags'] ||= 0 si_hash['startf_flags'] |= STARTF_USESTDHANDLES hash['inherit'] = true end } - + # The bytes not covered here are reserved (null) unless si_hash.empty? startinfo[0,4] = [startinfo.size].pack('L') @@ -350,7 +381,7 @@ def create(args) startinfo[48,2] = [si_hash['sw_flags']].pack('S') if si_hash['sw_flags'] startinfo[56,4] = [si_hash['stdin']].pack('L') if si_hash['stdin'] startinfo[60,4] = [si_hash['stdout']].pack('L') if si_hash['stdout'] - startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr'] + startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr'] end if hash['with_logon'] @@ -360,7 +391,7 @@ def create(args) cmd = hash['command_line'].nil? ? nil : multi_to_wide(hash['command_line']) cwd = multi_to_wide(hash['cwd']) passwd = multi_to_wide(hash['password']) - + hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT process_ran = CreateProcessWithLogonW( @@ -376,7 +407,7 @@ def create(args) startinfo, # Startup Info procinfo # Process Info ) - else + else process_ran = CreateProcess( hash['app_name'], # App name hash['command_line'], # Command line @@ -389,21 +420,21 @@ def create(args) startinfo, # Startup Info procinfo # Process Info ) - end + end # TODO: Close stdin, stdout and stderr handles in the si_hash unless # they're pointing to one of the standard handles already. [Maybe] if !process_ran raise_last_error("CreateProcess()") end - + # Automatically close the process and thread handles in the # PROCESS_INFORMATION struct unless explicitly told not to. if hash['close_handles'] CloseHandle(procinfo[0,4].unpack('L').first) CloseHandle(procinfo[4,4].unpack('L').first) - end - + end + ProcessInfo.new( procinfo[0,4].unpack('L').first, # hProcess procinfo[4,4].unpack('L').first, # hThread @@ -554,4 +585,4 @@ def self.raise_last_error(operation) } module_function :create -end \ No newline at end of file +end