Skip to content

Commit

Permalink
[CHEF-2819] fixes for Chef::ShellOut::Windows in 0.10.6
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
schisamo committed Dec 16, 2011
1 parent 02f7757 commit 1033f0b
Showing 1 changed file with 58 additions and 27 deletions.
85 changes: 58 additions & 27 deletions chef/lib/chef/shell_out/windows.rb
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -231,18 +262,18 @@ 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)
raise ArgumentError, "invalid key '#{key}'"
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|
Expand All @@ -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']
Expand Down Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -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']
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -554,4 +585,4 @@ def self.raise_last_error(operation)
}

module_function :create
end
end

0 comments on commit 1033f0b

Please sign in to comment.