summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <jkeiser@opscode.com>2011-10-13 14:56:43 -0700
committerJohn Keiser <jkeiser@opscode.com>2011-10-13 14:56:43 -0700
commit0ec5c98e2a6708e6aa0ee9a189205146ec60060f (patch)
tree01fe23c5ca00134ce6004021473da45bb5f5bef0
parentf1eb3c0a48a313afab154879a7c9bd13551fe9cd (diff)
downloadchef-0ec5c98e2a6708e6aa0ee9a189205146ec60060f.tar.gz
Add cwd and environment support for Windows shell_out
-rw-r--r--chef/lib/chef/shell_out.rb1
-rw-r--r--chef/lib/chef/shell_out/windows.rb356
2 files changed, 303 insertions, 54 deletions
diff --git a/chef/lib/chef/shell_out.rb b/chef/lib/chef/shell_out.rb
index 7cb7bfc371..90ebdc4ff2 100644
--- a/chef/lib/chef/shell_out.rb
+++ b/chef/lib/chef/shell_out.rb
@@ -21,7 +21,6 @@ require 'tmpdir'
require 'chef/log'
require 'fcntl'
require 'chef/exceptions'
-require 'chef/shell_out/unix'
class Chef
diff --git a/chef/lib/chef/shell_out/windows.rb b/chef/lib/chef/shell_out/windows.rb
index e923fe4074..8d4866369e 100644
--- a/chef/lib/chef/shell_out/windows.rb
+++ b/chef/lib/chef/shell_out/windows.rb
@@ -1,5 +1,6 @@
#--
# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: John Keiser (<jkeiser@opscode.com>)
# Copyright:: Copyright (c) 2011 Opscode, Inc.
# License:: Apache License, Version 2.0
#
@@ -16,83 +17,332 @@
# limitations under the License.
#
-require 'timeout'
-if RUBY_VERSION =~ /^1\.8/
- require 'win32/open3'
-else
- require 'open3'
-end
+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
+
#--
# Missing lots of features from the UNIX version, such as
- # environment, cwd, etc.
+ # uid, etc.
def run_command
- # win32 open4 is really just open3.
- Open3.popen3(@command) do |stdin,stdout,stderr|
- @finished_stdout = false
- @finished_stderr = false
- stdin.close
- stdout.sync = true
- stderr.sync = true
-
- # TBH, I really don't know what this will do when it times out.
- # However, I'm powerless to make windows have non-blocking IO, so
- # thread party it is.
- Timeout.timeout(timeout) do
- out_reader = Thread.new do
- loop do
- read_stdout(stdout)
- break if @finished_stdout
- end
+
+ #
+ # Create pipes to capture stdout and stderr,
+ # and begin collecting data from them
+ #
+ stdout_read, stdout_write = IO.pipe
+ stderr_read, stderr_write = IO.pipe
+ stdout_thread = create_stdout_thread(stdout_read, stderr_read)
+
+ begin
+
+ #
+ # Set cwd, environment, appname, etc.
+ #
+ create_process_args = {
+ :app_name => command,
+ :creation_flags => Process::DETACHED_PROCESS,
+ :startup_info => {
+ :stdout => stdout_write,
+ :stderr => stderr_write
+ },
+ :close_handles => false
+ }
+ create_process_args[:cwd] = cwd if cwd
+ create_process_args[:environment] = environment.map { |k,v| "#{k}=#{v}" } if environment
+
+ #
+ # Start the process
+ #
+ # TODO this will not work with "echo" and "set." Consider using cmd /c to get that support.
+ process = Process.create(create_process_args)
+ begin
+
+ #
+ # Wait for it to finish
+ #
+ wait_status = WaitForSingleObject(process.process_handle, timeout*1000)
+ 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 = exit_code.unpack('l').first
+ when WAIT_TIMEOUT
+ # Kill the process
+ raise Chef::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}"
+ else
+ raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
end
- err_reader = Thread.new do
- loop do
- read_stderr(stderr)
- break if @finished_stderr
+
+ ensure
+ CloseHandle(process.thread_handle)
+ CloseHandle(process.process_handle)
+ end
+
+ ensure
+ # This ensures we finish writing, which will cause stdout_thread to complete
+ stdout_write.close
+ stderr_write.close
+
+ stdout_thread.join
+
+ stdout_read.close
+ stderr_read.close
+ end
+
+ self
+ end
+
+ private
+
+ def create_stdout_thread(stdout_read, stderr_read)
+ Thread.new do
+ # emulates blocking read (readpartial).
+ streams = [stdout_read, stderr_read]
+ while streams.length > 0 && ready = IO.select(streams, nil, nil, READ_WAIT_TIME)
+ if ready.first.include?(stdout_read)
+ begin
+ @stdout << stdout_read.readpartial(READ_SIZE)
+ rescue EOFError
+ streams.delete(stdout_read)
end
end
- out_reader.join
- err_reader.join
+ if ready.first.include?(stderr_read)
+ begin
+ @stderr << stderr_read.readpartial(READ_SIZE)
+ rescue EOFError
+ streams.delete(stderr_read)
+ end
+ end
end
end
+ end
+
+ end # class
+ end
+end
- @status = $?
+#
+# 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
+ /
- self
+ 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
+ /
- rescue Timeout::Error
- raise Chef::Exceptions::CommandTimeout, "command timed out:\n#{format_for_exception}"
+ # 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
-
- def read_stdout(stdout)
- return nil if @finished_stdout
- if chunk = stdout.sysread(8096)
- @stdout << chunk
- else
- @finished_stdout = true
+ 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
- rescue EOFError
- @finished_stdout = true
- rescue Errno::EAGAIN
+ 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
- def read_stderr(stderr)
- return nil if @finished_stderr
- if chunk = stderr.sysread(8096)
- @stderr << chunk
+ 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
- @finished_stderr = true
+ handle = get_osfhandle(si_hash[io])
+ end
+
+ if handle == INVALID_HANDLE_VALUE
+ raise Error, get_last_error
end
- rescue EOFError
- @finished_stderr = true
- rescue Errno::EAGAIN
+
+ # 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
+
+ bool = 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
+ bool = 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]
+ unless bool
+ raise Error, "CreateProcess() failed: " + get_last_error
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
-end
+
+ module_function :create
+end \ No newline at end of file