diff options
author | John Keiser <jkeiser@opscode.com> | 2011-10-13 14:56:43 -0700 |
---|---|---|
committer | John Keiser <jkeiser@opscode.com> | 2011-10-13 14:56:43 -0700 |
commit | 0ec5c98e2a6708e6aa0ee9a189205146ec60060f (patch) | |
tree | 01fe23c5ca00134ce6004021473da45bb5f5bef0 | |
parent | f1eb3c0a48a313afab154879a7c9bd13551fe9cd (diff) | |
download | chef-0ec5c98e2a6708e6aa0ee9a189205146ec60060f.tar.gz |
Add cwd and environment support for Windows shell_out
-rw-r--r-- | chef/lib/chef/shell_out.rb | 1 | ||||
-rw-r--r-- | chef/lib/chef/shell_out/windows.rb | 356 |
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 |