Created
December 15, 2011 22:45
-
-
Save jkeiser/1483297 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#-- | |
# Author:: Daniel DeLeo (<[email protected]>) | |
# Author:: John Keiser (<[email protected]>) | |
# Copyright:: Copyright (c) 2011 Opscode, Inc. | |
# License:: Apache License, Version 2.0 | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
# | |
require 'win32/process' | |
require 'windows/handle' | |
require 'windows/process' | |
require 'windows/synchronize' | |
class Chef | |
class ShellOut | |
module Windows | |
include ::Windows::Handle | |
include ::Windows::Process | |
include ::Windows::Synchronize | |
TIME_SLICE = 0.05 | |
#-- | |
# Missing lots of features from the UNIX version, such as | |
# uid, etc. | |
def run_command | |
# | |
# Create pipes to capture stdout and stderr, | |
# | |
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 | |
# | |
# Set cwd, environment, appname, etc. | |
# | |
app_name, command_line = command_to_run | |
create_process_args = { | |
:app_name => app_name, | |
:command_line => command_line, | |
:startup_info => { | |
:stdout => stdout_write, | |
:stderr => stderr_write, | |
:stdin => stdin_read | |
}, | |
:environment => inherit_environment.map { |k,v| "#{k}=#{v}" }, | |
:close_handles => false | |
} | |
create_process_args[:cwd] = cwd if cwd | |
# | |
# Start the process | |
# | |
process = Process.create(create_process_args) | |
begin | |
# | |
# Wait for the process to finish, consuming output as we go | |
# | |
start_wait = Time.now | |
while true | |
wait_status = WaitForSingleObject(process.process_handle, 0) | |
case wait_status | |
when WAIT_OBJECT_0 | |
# Get process exit code | |
exit_code = [0].pack('l') | |
unless GetExitCodeProcess(process.process_handle, exit_code) | |
raise get_last_error | |
end | |
@status = ThingThatLooksSortOfLikeAProcessStatus.new | |
@status.exitstatus = exit_code.unpack('l').first | |
return self | |
when WAIT_TIMEOUT | |
# Kill the process | |
if (Time.now - start_wait) > timeout | |
raise Chef::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}" | |
end | |
consume_output(open_streams, stdout_read, stderr_read) | |
else | |
raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}" | |
end | |
end | |
ensure | |
CloseHandle(process.thread_handle) | |
CloseHandle(process.process_handle) | |
end | |
ensure | |
# | |
# Consume all remaining data from the pipes until they are closed | |
# | |
stdout_write.close | |
stderr_write.close | |
while consume_output(open_streams, stdout_read, stderr_read) | |
end | |
end | |
end | |
private | |
class ThingThatLooksSortOfLikeAProcessStatus | |
attr_accessor :exitstatus | |
def success? | |
exitstatus == 0 | |
end | |
end | |
def consume_output(open_streams, stdout_read, stderr_read) | |
return false if open_streams.length == 0 | |
ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME) | |
return true if ! ready | |
if ready.first.include?(stdout_read) | |
begin | |
next_chunk = stdout_read.readpartial(READ_SIZE) | |
@stdout << next_chunk | |
@live_stream << next_chunk if @live_stream | |
rescue EOFError | |
stdout_read.close | |
open_streams.delete(stdout_read) | |
end | |
end | |
if ready.first.include?(stderr_read) | |
begin | |
@stderr << stderr_read.readpartial(READ_SIZE) | |
rescue EOFError | |
stderr_read.close | |
open_streams.delete(stderr_read) | |
end | |
end | |
return true | |
end | |
IS_BATCH_FILE = /\.bat|\.cmd$/i | |
def command_to_run | |
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 | |
[ exe, command ] | |
end | |
end | |
def inherit_environment | |
result = {} | |
ENV.each_pair do |k,v| | |
result[k] = v | |
end | |
environment.each_pair do |k,v| | |
if v == nil | |
result.delete(k) | |
else | |
result[k] = v | |
end | |
end | |
result | |
end | |
def pathext | |
@pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : [''] | |
end | |
def which(cmd) | |
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| | |
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 { |ext| | |
exe = "#{path}#{ext}" | |
return exe if File.executable? exe | |
} | |
end | |
end # class | |
end | |
end | |
# | |
# Override module Windows::Process.CreateProcess to fix bug when | |
# using both app_name and command_line | |
# | |
module Windows | |
module Process | |
API.new('CreateProcess', 'SPPPLLLPPP', 'B') | |
end | |
end | |
# | |
# Override Win32::Process.create to take a proper environment hash | |
# so that variables can contain semicolons | |
# (submitted patch to owner) | |
# | |
module Process | |
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 | |
domain password | |
/ | |
valid_si_keys = %/ | |
startf_flags desktop title x y x_size y_size x_count_chars | |
y_count_chars fill_attribute sw_flags stdin stdout stderr | |
/ | |
# Set default values | |
hash = { | |
'app_name' => nil, | |
'creation_flags' => 0, | |
'close_handles' => true | |
} | |
# 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| | |
key = key.to_s.downcase | |
unless valid_si_keys.include?(key) | |
raise ArgumentError, "invalid startup_info key '#{key}'" | |
end | |
si_hash[key] = val | |
} | |
end | |
# The +command_line+ key is mandatory unless the +app_name+ key | |
# is specified. | |
unless hash['command_line'] | |
if hash['app_name'] | |
hash['command_line'] = hash['app_name'] | |
hash['app_name'] = nil | |
else | |
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'] | |
env = hash['environment'] | |
if !env.respond_to?(:join) | |
# Backwards compat for ; separated paths | |
env = hash['environment'].split(File::PATH_SEPARATOR) | |
end | |
# The argument format is a series of null-terminated strings, with an additional null terminator. | |
env = env.map { |e| e + "\0" }.join("") + "\0" | |
if hash['with_logon'] | |
env = env.multi_to_wide(e) | |
end | |
env = [env].pack('p*').unpack('L').first | |
else | |
env = nil | |
end | |
startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] | |
startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL') | |
procinfo = [0,0,0,0].pack('LLLL') | |
# Process SECURITY_ATTRIBUTE structure | |
process_security = 0 | |
if hash['process_inherit'] | |
process_security = [0,0,0].pack('LLL') | |
process_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE) | |
process_security[8,4] = [1].pack('L') # TRUE | |
end | |
# Thread SECURITY_ATTRIBUTE structure | |
thread_security = 0 | |
if hash['thread_inherit'] | |
thread_security = [0,0,0].pack('LLL') | |
thread_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE) | |
thread_security[8,4] = [1].pack('L') # TRUE | |
end | |
# Automatically handle stdin, stdout and stderr as either IO objects | |
# or file descriptors. This won't work for StringIO, however. | |
['stdin', 'stdout', 'stderr'].each{ |io| | |
if si_hash[io] | |
if si_hash[io].respond_to?(:fileno) | |
handle = get_osfhandle(si_hash[io].fileno) | |
else | |
handle = get_osfhandle(si_hash[io]) | |
end | |
if handle == INVALID_HANDLE_VALUE | |
raise Error, get_last_error | |
end | |
# Most implementations of Ruby on Windows create inheritable | |
# handles by default, but some do not. RF bug #26988. | |
bool = SetHandleInformation( | |
handle, | |
HANDLE_FLAG_INHERIT, | |
HANDLE_FLAG_INHERIT | |
) | |
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') | |
startinfo[8,4] = [si_hash['desktop']].pack('p*') if si_hash['desktop'] | |
startinfo[12,4] = [si_hash['title']].pack('p*') if si_hash['title'] | |
startinfo[16,4] = [si_hash['x']].pack('L') if si_hash['x'] | |
startinfo[20,4] = [si_hash['y']].pack('L') if si_hash['y'] | |
startinfo[24,4] = [si_hash['x_size']].pack('L') if si_hash['x_size'] | |
startinfo[28,4] = [si_hash['y_size']].pack('L') if si_hash['y_size'] | |
startinfo[32,4] = [si_hash['x_count_chars']].pack('L') if si_hash['x_count_chars'] | |
startinfo[36,4] = [si_hash['y_count_chars']].pack('L') if si_hash['y_count_chars'] | |
startinfo[40,4] = [si_hash['fill_attribute']].pack('L') if si_hash['fill_attribute'] | |
startinfo[44,4] = [si_hash['startf_flags']].pack('L') if si_hash['startf_flags'] | |
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'] | |
end | |
if hash['with_logon'] | |
logon = multi_to_wide(hash['with_logon']) | |
domain = multi_to_wide(hash['domain']) | |
app = hash['app_name'].nil? ? nil : multi_to_wide(hash['app_name']) | |
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( | |
logon, # User | |
domain, # Domain | |
passwd, # Password | |
LOGON_WITH_PROFILE, # Logon flags | |
app, # App name | |
cmd, # Command line | |
hash['creation_flags'], # Creation flags | |
env, # Environment | |
cwd, # Working directory | |
startinfo, # Startup Info | |
procinfo # Process Info | |
) | |
else | |
process_ran = CreateProcess( | |
hash['app_name'], # App name | |
hash['command_line'], # Command line | |
process_security, # Process attributes | |
thread_security, # Thread attributes | |
hash['inherit'], # Inherit handles? | |
hash['creation_flags'], # Creation flags | |
env, # Environment | |
hash['cwd'], # Working directory | |
startinfo, # Startup Info | |
procinfo # Process Info | |
) | |
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 | |
ProcessInfo.new( | |
procinfo[0,4].unpack('L').first, # hProcess | |
procinfo[4,4].unpack('L').first, # hThread | |
procinfo[8,4].unpack('L').first, # hProcessId | |
procinfo[12,4].unpack('L').first # hThreadId | |
) | |
end | |
def self.raise_last_error(operation) | |
error_string = "#{operation} failed: #{get_last_error}" | |
last_error_code = GetLastError() | |
if ERROR_CODE_MAP.has_key?(last_error_code) | |
raise ERROR_CODE_MAP[last_error_code], error_string | |
else | |
raise Error, error_string | |
end | |
end | |
# List from ruby/win32/win32.c | |
ERROR_CODE_MAP = { | |
ERROR_INVALID_FUNCTION => Errno::EINVAL, | |
ERROR_FILE_NOT_FOUND => Errno::ENOENT, | |
ERROR_PATH_NOT_FOUND => Errno::ENOENT, | |
ERROR_TOO_MANY_OPEN_FILES => Errno::EMFILE, | |
ERROR_ACCESS_DENIED => Errno::EACCES, | |
ERROR_INVALID_HANDLE => Errno::EBADF, | |
ERROR_ARENA_TRASHED => Errno::ENOMEM, | |
ERROR_NOT_ENOUGH_MEMORY => Errno::ENOMEM, | |
ERROR_INVALID_BLOCK => Errno::ENOMEM, | |
ERROR_BAD_ENVIRONMENT => Errno::E2BIG, | |
ERROR_BAD_FORMAT => Errno::ENOEXEC, | |
ERROR_INVALID_ACCESS => Errno::EINVAL, | |
ERROR_INVALID_DATA => Errno::EINVAL, | |
ERROR_INVALID_DRIVE => Errno::ENOENT, | |
ERROR_CURRENT_DIRECTORY => Errno::EACCES, | |
ERROR_NOT_SAME_DEVICE => Errno::EXDEV, | |
ERROR_NO_MORE_FILES => Errno::ENOENT, | |
ERROR_WRITE_PROTECT => Errno::EROFS, | |
ERROR_BAD_UNIT => Errno::ENODEV, | |
ERROR_NOT_READY => Errno::ENXIO, | |
ERROR_BAD_COMMAND => Errno::EACCES, | |
ERROR_CRC => Errno::EACCES, | |
ERROR_BAD_LENGTH => Errno::EACCES, | |
ERROR_SEEK => Errno::EIO, | |
ERROR_NOT_DOS_DISK => Errno::EACCES, | |
ERROR_SECTOR_NOT_FOUND => Errno::EACCES, | |
ERROR_OUT_OF_PAPER => Errno::EACCES, | |
ERROR_WRITE_FAULT => Errno::EIO, | |
ERROR_READ_FAULT => Errno::EIO, | |
ERROR_GEN_FAILURE => Errno::EACCES, | |
ERROR_LOCK_VIOLATION => Errno::EACCES, | |
ERROR_SHARING_VIOLATION => Errno::EACCES, | |
ERROR_WRONG_DISK => Errno::EACCES, | |
ERROR_SHARING_BUFFER_EXCEEDED => Errno::EACCES, | |
# ERROR_BAD_NETPATH => Errno::ENOENT, | |
# ERROR_NETWORK_ACCESS_DENIED => Errno::EACCES, | |
# ERROR_BAD_NET_NAME => Errno::ENOENT, | |
ERROR_FILE_EXISTS => Errno::EEXIST, | |
ERROR_CANNOT_MAKE => Errno::EACCES, | |
ERROR_FAIL_I24 => Errno::EACCES, | |
ERROR_INVALID_PARAMETER => Errno::EINVAL, | |
ERROR_NO_PROC_SLOTS => Errno::EAGAIN, | |
ERROR_DRIVE_LOCKED => Errno::EACCES, | |
ERROR_BROKEN_PIPE => Errno::EPIPE, | |
ERROR_DISK_FULL => Errno::ENOSPC, | |
ERROR_INVALID_TARGET_HANDLE => Errno::EBADF, | |
ERROR_INVALID_HANDLE => Errno::EINVAL, | |
ERROR_WAIT_NO_CHILDREN => Errno::ECHILD, | |
ERROR_CHILD_NOT_COMPLETE => Errno::ECHILD, | |
ERROR_DIRECT_ACCESS_HANDLE => Errno::EBADF, | |
ERROR_NEGATIVE_SEEK => Errno::EINVAL, | |
ERROR_SEEK_ON_DEVICE => Errno::EACCES, | |
ERROR_DIR_NOT_EMPTY => Errno::ENOTEMPTY, | |
# ERROR_DIRECTORY => Errno::ENOTDIR, | |
ERROR_NOT_LOCKED => Errno::EACCES, | |
ERROR_BAD_PATHNAME => Errno::ENOENT, | |
ERROR_MAX_THRDS_REACHED => Errno::EAGAIN, | |
# ERROR_LOCK_FAILED => Errno::EACCES, | |
ERROR_ALREADY_EXISTS => Errno::EEXIST, | |
ERROR_INVALID_STARTING_CODESEG => Errno::ENOEXEC, | |
ERROR_INVALID_STACKSEG => Errno::ENOEXEC, | |
ERROR_INVALID_MODULETYPE => Errno::ENOEXEC, | |
ERROR_INVALID_EXE_SIGNATURE => Errno::ENOEXEC, | |
ERROR_EXE_MARKED_INVALID => Errno::ENOEXEC, | |
ERROR_BAD_EXE_FORMAT => Errno::ENOEXEC, | |
ERROR_ITERATED_DATA_EXCEEDS_64k => Errno::ENOEXEC, | |
ERROR_INVALID_MINALLOCSIZE => Errno::ENOEXEC, | |
ERROR_DYNLINK_FROM_INVALID_RING => Errno::ENOEXEC, | |
ERROR_IOPL_NOT_ENABLED => Errno::ENOEXEC, | |
ERROR_INVALID_SEGDPL => Errno::ENOEXEC, | |
ERROR_AUTODATASEG_EXCEEDS_64k => Errno::ENOEXEC, | |
ERROR_RING2SEG_MUST_BE_MOVABLE => Errno::ENOEXEC, | |
ERROR_RELOC_CHAIN_XEEDS_SEGLIM => Errno::ENOEXEC, | |
ERROR_INFLOOP_IN_RELOC_CHAIN => Errno::ENOEXEC, | |
ERROR_FILENAME_EXCED_RANGE => Errno::ENOENT, | |
ERROR_NESTING_NOT_ALLOWED => Errno::EAGAIN, | |
# ERROR_PIPE_LOCAL => Errno::EPIPE, | |
ERROR_BAD_PIPE => Errno::EPIPE, | |
ERROR_PIPE_BUSY => Errno::EAGAIN, | |
ERROR_NO_DATA => Errno::EPIPE, | |
ERROR_PIPE_NOT_CONNECTED => Errno::EPIPE, | |
ERROR_OPERATION_ABORTED => Errno::EINTR, | |
# ERROR_NOT_ENOUGH_QUOTA => Errno::ENOMEM, | |
ERROR_MOD_NOT_FOUND => Errno::ENOENT, | |
WSAEINTR => Errno::EINTR, | |
WSAEBADF => Errno::EBADF, | |
# WSAEACCES => Errno::EACCES, | |
WSAEFAULT => Errno::EFAULT, | |
WSAEINVAL => Errno::EINVAL, | |
WSAEMFILE => Errno::EMFILE, | |
WSAEWOULDBLOCK => Errno::EWOULDBLOCK, | |
WSAEINPROGRESS => Errno::EINPROGRESS, | |
WSAEALREADY => Errno::EALREADY, | |
WSAENOTSOCK => Errno::ENOTSOCK, | |
WSAEDESTADDRREQ => Errno::EDESTADDRREQ, | |
WSAEMSGSIZE => Errno::EMSGSIZE, | |
WSAEPROTOTYPE => Errno::EPROTOTYPE, | |
WSAENOPROTOOPT => Errno::ENOPROTOOPT, | |
WSAEPROTONOSUPPORT => Errno::EPROTONOSUPPORT, | |
WSAESOCKTNOSUPPORT => Errno::ESOCKTNOSUPPORT, | |
WSAEOPNOTSUPP => Errno::EOPNOTSUPP, | |
WSAEPFNOSUPPORT => Errno::EPFNOSUPPORT, | |
WSAEAFNOSUPPORT => Errno::EAFNOSUPPORT, | |
WSAEADDRINUSE => Errno::EADDRINUSE, | |
WSAEADDRNOTAVAIL => Errno::EADDRNOTAVAIL, | |
WSAENETDOWN => Errno::ENETDOWN, | |
WSAENETUNREACH => Errno::ENETUNREACH, | |
WSAENETRESET => Errno::ENETRESET, | |
WSAECONNABORTED => Errno::ECONNABORTED, | |
WSAECONNRESET => Errno::ECONNRESET, | |
WSAENOBUFS => Errno::ENOBUFS, | |
WSAEISCONN => Errno::EISCONN, | |
WSAENOTCONN => Errno::ENOTCONN, | |
WSAESHUTDOWN => Errno::ESHUTDOWN, | |
WSAETOOMANYREFS => Errno::ETOOMANYREFS, | |
# WSAETIMEDOUT => Errno::ETIMEDOUT, | |
WSAECONNREFUSED => Errno::ECONNREFUSED, | |
WSAELOOP => Errno::ELOOP, | |
WSAENAMETOOLONG => Errno::ENAMETOOLONG, | |
WSAEHOSTDOWN => Errno::EHOSTDOWN, | |
WSAEHOSTUNREACH => Errno::EHOSTUNREACH, | |
# WSAEPROCLIM => Errno::EPROCLIM, | |
# WSAENOTEMPTY => Errno::ENOTEMPTY, | |
WSAEUSERS => Errno::EUSERS, | |
WSAEDQUOT => Errno::EDQUOT, | |
WSAESTALE => Errno::ESTALE, | |
WSAEREMOTE => Errno::EREMOTE | |
} | |
module_function :create | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment