[DRE-commits] [ruby-mixlib-shellout] 02/10: Imported Upstream version 2.1.0

Antonio Terceiro terceiro at moszumanska.debian.org
Sat Jun 13 16:30:52 UTC 2015


This is an automated email from the git hooks/post-receive script.

terceiro pushed a commit to branch master
in repository ruby-mixlib-shellout.

commit ea5c1274bb54199e1b5878c91fdd9bde82b37d53
Author: Antonio Terceiro <terceiro at debian.org>
Date:   Fri Jun 12 22:55:05 2015 -0300

    Imported Upstream version 2.1.0
---
 README.md                               |   6 +-
 lib/mixlib/shellout.rb                  | 104 +++++++++++++------
 lib/mixlib/shellout/unix.rb             | 176 ++++++++++++++++++++++----------
 lib/mixlib/shellout/version.rb          |   2 +-
 lib/mixlib/shellout/windows.rb          |  17 ++-
 lib/mixlib/shellout/windows/core_ext.rb |  41 +++++---
 metadata.yml                            |  32 +++---
 7 files changed, 250 insertions(+), 128 deletions(-)

diff --git a/README.md b/README.md
index 1f0e46f..2cfe5b0 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ Invoke crontab to edit user cron:
       crontab.run_command
 
 ## Windows Impersonation Example
-Invoke crontab to edit user cron:
+Invoke "whoami.exe" to demonstrate running a command as another user:
 
-      whomai = Mixlib::ShellOut.new("whoami.exe", :user => "username", :domain => "DOMAIN", :password => "password")
+      whoami = Mixlib::ShellOut.new("whoami.exe", :user => "username", :domain => "DOMAIN", :password => "password")
       whoami.run_command      
 
 ## Platform Support
@@ -51,4 +51,4 @@ Apache 2 Licensed. See LICENSE for full details.
 
 ## See Also
 * `Process.spawn` in Ruby 1.9
-* [https://github.com/rtomayko/posix-spawn](posix-spawn)
+* [https://github.com/rtomayko/posix-spawn](https://github.com/rtomayko/posix-spawn)
diff --git a/lib/mixlib/shellout.rb b/lib/mixlib/shellout.rb
index 66faa40..c77fdcf 100644
--- a/lib/mixlib/shellout.rb
+++ b/lib/mixlib/shellout.rb
@@ -27,7 +27,6 @@ module Mixlib
     READ_WAIT_TIME = 0.01
     READ_SIZE = 4096
     DEFAULT_READ_TIMEOUT = 600
-    DEFAULT_ENVIRONMENT = {'LC_ALL' => 'C'}
 
     if RUBY_PLATFORM =~ /mswin|mingw32|windows/
       require 'mixlib/shellout/windows'
@@ -41,22 +40,30 @@ module Mixlib
     attr_accessor :user
     attr_accessor :domain
     attr_accessor :password
+    # TODO remove
     attr_accessor :with_logon
 
+    # Whether to simulate logon as the user. Normally set via options passed to new
+    # Always enabled on windows
+    attr_accessor :login
+
     # Group the command will run as. Normally set via options passed to new
     attr_accessor :group
 
     # Working directory for the subprocess. Normally set via options to new
     attr_accessor :cwd
 
-    # An Array of acceptable exit codes. #error! uses this list to determine if
-    # the command was successful. Normally set via options to new
+    # An Array of acceptable exit codes. #error? (and #error!) use this list
+    # to determine if the command was successful. Normally set via options to new
     attr_accessor :valid_exit_codes
 
-    # When live_stream is set, stdout of the subprocess will be copied to it as
-    # the subprocess is running. For example, if live_stream is set to STDOUT,
-    # the command's output will be echoed to STDOUT.
-    attr_accessor :live_stream
+    # When live_stdout is set, the stdout of the subprocess will be copied to it
+    # as the subprocess is running.
+    attr_accessor :live_stdout
+
+    # When live_stderr is set, the stderr of the subprocess will be copied to it
+    # as the subprocess is running.
+    attr_accessor :live_stderr
 
     # ShellOut will push data from :input down the stdin of the subprocss.
     # Normally set via options passed to new.
@@ -81,7 +88,7 @@ module Mixlib
 
     # Environment variables that will be set for the subcommand. Refer to the
     # documentation of new to understand how ShellOut interprets this.
-    attr_reader :environment
+    attr_accessor :environment
 
     # The maximum time this command is allowed to run. Usually set via options
     # to new
@@ -122,16 +129,25 @@ module Mixlib
     #   subprocess. This only has an effect if you call +error!+ after
     #   +run_command+.
     # * +environment+: a Hash of environment variables to set before the command
-    #   is run. By default, the environment will *always* be set to 'LC_ALL' => 'C'
-    #   to prevent issues with multibyte characters in Ruby 1.8. To avoid this,
-    #   use :environment => nil for *no* extra environment settings, or
-    #   :environment => {'LC_ALL'=>nil, ...} to set other environment settings
-    #   without changing the locale.
+    #   is run.
     # * +timeout+: a Numeric value for the number of seconds to wait on the
     #   child process before raising an Exception. This is calculated as the
     #   total amount of time that ShellOut waited on the child process without
     #   receiving any output (i.e., IO.select returned nil). Default is 60
     #   seconds. Note: the stdlib Timeout library is not used.
+    # * +input+: A String of data to be passed to the subcommand. This is
+    #   written to the child process' stdin stream before the process is
+    #   launched. The child's stdin stream will be a pipe, so the size of input
+    #   data should not exceed the system's default pipe capacity (4096 bytes
+    #   is a safe value, though on newer Linux systems the capacity is 64k by
+    #   default).
+    # * +live_stream+: An IO or Logger-like object (must respond to the append
+    #   operator +<<+) that will receive data as ShellOut reads it from the
+    #   child process. Generally this is used to copy data from the child to
+    #   the parent's stdout so that users may observe the progress of
+    #   long-running commands.
+    # * +login+: Whether to simulate a login (set secondary groups, primary group, environment
+    #   variables etc) as done by the OS in an actual login
     # === Examples:
     # Invoke find(1) to search for .rb files:
     #   find = Mixlib::ShellOut.new("find . -name '*.rb'")
@@ -146,15 +162,16 @@ module Mixlib
     #   cmd = Mixlib::ShellOut.new("apachectl", "start", :user => 'www', :env => nil, :cwd => '/tmp')
     #   cmd.run_command # etc.
     def initialize(*command_args)
-      @stdout, @stderr = '', ''
-      @live_stream = nil
+      @stdout, @stderr, @process_status = '', '', ''
+      @live_stdout = @live_stderr = nil
       @input = nil
       @log_level = :debug
       @log_tag = nil
-      @environment = DEFAULT_ENVIRONMENT
+      @environment = {}
       @cwd = nil
       @valid_exit_codes = [0]
       @terminate_reason = nil
+      @timeout = nil
 
       if command_args.last.is_a?(Hash)
         parse_options(command_args.pop)
@@ -163,6 +180,18 @@ module Mixlib
       @command = command_args.size == 1 ? command_args.first : command_args
     end
 
+    # Returns the stream that both is being used by both live_stdout and live_stderr, or nil
+    def live_stream
+      live_stdout == live_stderr ? live_stdout : nil
+    end
+
+    # A shortcut for setting both live_stdout and live_stderr, so that both the
+    # stdout and stderr from the subprocess will be copied to the same stream as
+    # the subprocess is running.
+    def live_stream=(stream)
+      @live_stdout = @live_stderr = stream
+    end
+
     # Set the umask that the subprocess will have. If given as a string, it
     # will be converted to an integer by String#oct.
     def umask=(new_umask)
@@ -171,6 +200,7 @@ module Mixlib
 
     # The uid that the subprocess will switch to. If the user attribute was
     # given as a username, it is converted to a uid by Etc.getpwnam
+    # TODO migrate to shellout/unix.rb
     def uid
       return nil unless user
       user.kind_of?(Integer) ? user : Etc.getpwnam(user.to_s).uid
@@ -178,9 +208,11 @@ module Mixlib
 
     # The gid that the subprocess will switch to. If the group attribute is
     # given as a group name, it is converted to a gid by Etc.getgrnam
+    # TODO migrate to shellout/unix.rb
     def gid
-      return nil unless group
-      group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid
+      return group.kind_of?(Integer) ? group : Etc.getgrnam(group.to_s).gid if group
+      return Etc.getpwuid(uid).gid if using_login?
+      return nil
     end
 
     def timeout
@@ -227,17 +259,21 @@ module Mixlib
       super
     end
 
-    # Checks the +exitstatus+ against the set of +valid_exit_codes+. If
-    # +exitstatus+ is not in the list of +valid_exit_codes+, calls +invalid!+,
-    # which raises an Exception.
+    # Checks the +exitstatus+ against the set of +valid_exit_codes+.
+    # === Returns
+    # +true+ if +exitstatus+ is not in the list of +valid_exit_codes+, false
+    # otherwise.
+    def error?
+      !Array(valid_exit_codes).include?(exitstatus)
+    end
+
+    # If #error? is true, calls +invalid!+, which raises an Exception.
     # === Returns
     # nil::: always returns nil when it does not raise
     # === Raises
     # ::ShellCommandFailed::: via +invalid!+
     def error!
-      unless Array(valid_exit_codes).include?(exitstatus)
-        invalid!("Expected process to exit with #{valid_exit_codes.inspect}, but received '#{exitstatus}'")
-      end
+      invalid!("Expected process to exit with #{valid_exit_codes.inspect}, but received '#{exitstatus}'") if error?
     end
 
     # Raises a ShellCommandFailed exception, appending the
@@ -282,7 +318,11 @@ module Mixlib
         when 'returns'
           self.valid_exit_codes = Array(setting)
         when 'live_stream'
-          self.live_stream = setting
+          self.live_stdout = self.live_stderr = setting
+        when 'live_stdout'
+          self.live_stdout = setting
+        when 'live_stderr'
+          self.live_stderr = setting
         when 'input'
           self.input = setting
         when 'logger'
@@ -292,14 +332,9 @@ module Mixlib
         when 'log_tag'
           self.log_tag = setting
         when 'environment', 'env'
-          # Set the LC_ALL from the parent process if the user wanted
-          # to use the default.
-          if setting && setting.has_key?("LC_ALL") && setting['LC_ALL'].nil?
-            setting['LC_ALL'] = ENV['LC_ALL']
-          end
-          # passing :environment => nil means don't set any new ENV vars
-          @environment = setting.nil? ? {} : @environment.dup.merge!(setting)
-
+          self.environment = setting || {}
+        when 'login'
+          self.login = setting
         else
           raise InvalidCommandOption, "option '#{option.inspect}' is not a valid option for #{self.class.name}"
         end
@@ -309,6 +344,9 @@ module Mixlib
     end
 
     def validate_options(opts)
+      if login && !user
+        raise InvalidCommandOption, "cannot set login without specifying a user"
+      end
       super
     end
   end
diff --git a/lib/mixlib/shellout/unix.rb b/lib/mixlib/shellout/unix.rb
index 15ead60..dd22cbe 100644
--- a/lib/mixlib/shellout/unix.rb
+++ b/lib/mixlib/shellout/unix.rb
@@ -20,22 +20,75 @@ module Mixlib
   class ShellOut
     module Unix
 
+      # "1.8.7" as a frozen string. We use this with a hack that disables GC to
+      # avoid segfaults on Ruby 1.8.7, so we need to allocate the fewest
+      # objects we possibly can.
+      ONE_DOT_EIGHT_DOT_SEVEN = "1.8.7".freeze
+
       # Option validation that is unix specific
       def validate_options(opts)
         # No options to validate, raise exceptions here if needed
       end
 
+      # Whether we're simulating a login shell
+      def using_login?
+        return login && user
+      end
+
+      # Helper method for sgids
+      def all_seconderies
+        ret = []
+        Etc.endgrent
+        while ( g = Etc.getgrent ) do
+          ret << g
+        end
+        Etc.endgrent
+        return ret
+      end
+
+      # The secondary groups that the subprocess will switch to.
+      # Currently valid only if login is used, and is set
+      # to the user's secondary groups
+      def sgids
+        return nil unless using_login?
+        user_name = Etc.getpwuid(uid).name
+        all_seconderies.select{|g| g.mem.include?(user_name)}.map{|g|g.gid}
+      end
+
+      # The environment variables that are deduced from simulating logon
+      # Only valid if login is used
+      def logon_environment
+        return {} unless using_login?
+        entry = Etc.getpwuid(uid)
+        # According to `man su`, the set fields are:
+        #  $HOME, $SHELL, $USER, $LOGNAME, $PATH, and $IFS
+        # Values are copied from "shadow" package in Ubuntu 14.10
+        {'HOME'=>entry.dir, 'SHELL'=>entry.shell, 'USER'=>entry.name, 'LOGNAME'=>entry.name, 'PATH'=>'/sbin:/bin:/usr/sbin:/usr/bin', 'IFS'=>"\t\n"}
+      end
+
+      # Merges the two environments for the process
+      def process_environment
+        logon_environment.merge(self.environment)
+      end
+
       # Run the command, writing the command's standard out and standard error
       # to +stdout+ and +stderr+, and saving its exit status object to +status+
       # === Returns
       # returns   +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be
-      # populated with results of the command
+      # populated with results of the command.
       # === Raises
       # * Errno::EACCES  when you are not privileged to execute the command
       # * Errno::ENOENT  when the command is not available on the system (or not
       #   in the current $PATH)
       # * Chef::Exceptions::CommandTimeout  when the command does not complete
-      #   within +timeout+ seconds (default: 600s)
+      #   within +timeout+ seconds (default: 600s). When this happens, ShellOut
+      #   will send a TERM and then KILL to the entire process group to ensure
+      #   that any grandchild processes are terminated. If the invocation of
+      #   the child process spawned multiple child processes (which commonly
+      #   happens if the command is passed as a single string to be interpreted
+      #   by bin/sh, and bin/sh is not bash), the exit status object may not
+      #   contain the correct exit code of the process (of course there is no
+      #   exit code if the command is killed by SIGKILL, also).
       def run_command
         @child_pid = fork_subprocess
         @reaped = false
@@ -43,15 +96,16 @@ module Mixlib
         configure_parent_process_file_descriptors
 
         # Ruby 1.8.7 and 1.8.6 from mid 2009 try to allocate objects during GC
-        # when calling IO.select and IO#read. Some OS Vendors are not interested
-        # in updating their ruby packages (Apple, *cough*) and we *have to*
-        # make it work. So I give you this epic hack:
-        GC.disable
+        # when calling IO.select and IO#read. Disabling GC works around the
+        # segfault, but obviously it's a bad workaround. We no longer support
+        # 1.8.6 so we only need this hack for 1.8.7.
+        GC.disable if RUBY_VERSION == ONE_DOT_EIGHT_DOT_SEVEN
 
         # CHEF-3390: Marshall.load on Ruby < 1.8.7p369 also has a GC bug related
         # to Marshall.load, so try disabling GC first.
         propagate_pre_exec_failure
 
+        @status = nil
         @result = nil
         @execution_time = 0
 
@@ -96,8 +150,8 @@ module Mixlib
 
       def set_user
         if user
-          Process.euid = uid
           Process.uid = uid
+          Process.euid = uid
         end
       end
 
@@ -108,8 +162,15 @@ module Mixlib
         end
       end
 
+      def set_secondarygroups
+        if sgids
+          Process.groups = sgids
+        end
+      end
+
       def set_environment
-        environment.each do |env_var,value|
+        # user-set variables should override the login ones
+        process_environment.each do |env_var,value|
           ENV[env_var] = value
         end
       end
@@ -122,6 +183,12 @@ module Mixlib
         Dir.chdir(cwd) if cwd
       end
 
+      # Since we call setsid the child_pgid will be the child_pid, set to negative here
+      # so it can be directly used in arguments to kill, wait, etc.
+      def child_pgid
+        - at child_pid
+      end
+
       def initialize_ipc
         @stdin_pipe, @stdout_pipe, @stderr_pipe, @process_status_pipe = IO.pipe, IO.pipe, IO.pipe, IO.pipe
         @process_status_pipe.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
@@ -176,31 +243,6 @@ module Mixlib
         STDIN.sync = true if input
       end
 
-      # When a new process is started with chef, it shares the file
-      # descriptors of the parent. We clean the file descriptors
-      # coming from the parent to prevent unintended locking if parent
-      # is killed.
-      # NOTE: After some discussions we've decided to iterate on file
-      # descriptors upto 256. We believe this  is a reasonable upper
-      # limit in a chef environment. If we have issues in the future this
-      # number could be made to be configurable or updated based on
-      # the ulimit based on platform.
-      def clean_parent_file_descriptors
-        # Don't clean $stdin, $stdout, $stderr, process_status_pipe.
-        3.upto(256) do |n|
-          # We are checking the fd for error pipe before attempting to
-          # create a file because error pipe will auto close when we
-          # try to create a file since it's set to CLOEXEC.
-          if n != @process_status_pipe.last.to_i
-            begin
-              fd = File.for_fd(n)
-              fd.close if fd
-            rescue
-            end
-          end
-        end
-      end
-
       def configure_parent_process_file_descriptors
         # Close the sides of the pipes we don't care about
         stdin_pipe.first.close
@@ -219,7 +261,7 @@ module Mixlib
       # Some patch levels of ruby in wide use (in particular the ruby 1.8.6 on OSX)
       # segfault when you IO.select a pipe that's reached eof. Weak sauce.
       def open_pipes
-        @open_pipes ||= [child_stdout, child_stderr]
+        @open_pipes ||= [child_stdout, child_stderr, child_process_status]
       end
 
       # Keep this unbuffered for now
@@ -231,11 +273,10 @@ module Mixlib
 
       def attempt_buffer_read
         ready = IO.select(open_pipes, nil, nil, READ_WAIT_TIME)
-        if ready && ready.first.include?(child_stdout)
-          read_stdout_to_buffer
-        end
-        if ready && ready.first.include?(child_stderr)
-          read_stderr_to_buffer
+        if ready
+          read_stdout_to_buffer if ready.first.include?(child_stdout)
+          read_stderr_to_buffer if ready.first.include?(child_stderr)
+          read_process_status_to_buffer if ready.first.include?(child_process_status)
         end
         ready
       end
@@ -243,7 +284,7 @@ module Mixlib
       def read_stdout_to_buffer
         while chunk = child_stdout.read_nonblock(READ_SIZE)
           @stdout << chunk
-          @live_stream << chunk if @live_stream
+          @live_stdout << chunk if @live_stdout
         end
       rescue Errno::EAGAIN
       rescue EOFError
@@ -253,20 +294,40 @@ module Mixlib
       def read_stderr_to_buffer
         while chunk = child_stderr.read_nonblock(READ_SIZE)
           @stderr << chunk
+          @live_stderr << chunk if @live_stderr
         end
       rescue Errno::EAGAIN
       rescue EOFError
         open_pipes.delete(child_stderr)
       end
 
+      def read_process_status_to_buffer
+        while chunk = child_process_status.read_nonblock(READ_SIZE)
+          @process_status << chunk
+        end
+      rescue Errno::EAGAIN
+      rescue EOFError
+        open_pipes.delete(child_process_status)
+      end
+
       def fork_subprocess
         initialize_ipc
 
         fork do
-          configure_subprocess_file_descriptors
+          # Child processes may themselves fork off children. A common case
+          # is when the command is given as a single string (instead of
+          # command name plus Array of arguments) and /bin/sh does not
+          # support the "ONESHOT" optimization (where sh -c does exec without
+          # forking). To support cleaning up all the children, we need to
+          # ensure they're in a unique process group.
+          #
+          # We use setsid here to abandon our controlling tty and get a new session
+          # and process group that are set to the pid of the child process.
+          Process.setsid
 
-          clean_parent_file_descriptors
+          configure_subprocess_file_descriptors
 
+          set_secondarygroups
           set_group
           set_user
           set_environment
@@ -274,7 +335,7 @@ module Mixlib
           set_cwd
 
           begin
-            command.kind_of?(Array) ? exec(*command) : exec(command)
+            command.kind_of?(Array) ? exec(*command, :close_others=>true) : exec(command, :close_others=>true)
 
             raise 'forty-two' # Should never get here
           rescue Exception => e
@@ -291,25 +352,26 @@ module Mixlib
       # assume everything went well.
       def propagate_pre_exec_failure
         begin
-          e = Marshal.load child_process_status
+          attempt_buffer_read until child_process_status.eof?
+          e = Marshal.load(@process_status)
           raise(Exception === e ? e : "unknown failure: #{e.inspect}")
-        rescue EOFError # If we get an EOF error, then the exec was successful
+        rescue ArgumentError # If we get an ArgumentError error, then the exec was successful
           true
         ensure
           child_process_status.close
+          open_pipes.delete(child_process_status)
         end
       end
 
       def reap_errant_child
         return if attempt_reap
-        @terminate_reason = "Command execeded allowed execution time, killed by TERM signal."
-        logger.error("Command execeded allowed execution time, sending TERM") if logger
-        Process.kill(:TERM, @child_pid)
+        @terminate_reason = "Command exceeded allowed execution time, process terminated"
+        logger.error("Command exceeded allowed execution time, sending TERM") if logger
+        Process.kill(:TERM, child_pgid)
         sleep 3
-        return if attempt_reap
-        @terminate_reason = "Command execeded allowed execution time, did not respond to TERM. Killed by KILL signal."
-        logger.error("Command did not exit from TERM, sending KILL") if logger
-        Process.kill(:KILL, @child_pid)
+        attempt_reap
+        logger.error("Command exceeded allowed execution time, sending KILL") if logger
+        Process.kill(:KILL, child_pgid)
         reap
 
         # Should not hit this but it's possible if something is calling waitall
@@ -323,12 +385,22 @@ module Mixlib
         @child_pid && !@reaped
       end
 
+      # Unconditionally reap the child process. This is used in scenarios where
+      # we can be confident the child will exit quickly, and has not spawned
+      # and grandchild processes.
       def reap
         results = Process.waitpid2(@child_pid)
         @reaped = true
         @status = results.last
+      rescue Errno::ECHILD
+        # When cleaning up timed-out processes, we might send SIGKILL to the
+        # whole process group after we've cleaned up the direct child. In that
+        # case the grandchildren will have been adopted by init so we can't
+        # reap them even if we wanted to (we don't).
+        nil
       end
 
+      # Try to reap the child process but don't block if it isn't dead yet.
       def attempt_reap
         if results = Process.waitpid2(@child_pid, Process::WNOHANG)
           @reaped = true
diff --git a/lib/mixlib/shellout/version.rb b/lib/mixlib/shellout/version.rb
index aef17a3..1092f06 100644
--- a/lib/mixlib/shellout/version.rb
+++ b/lib/mixlib/shellout/version.rb
@@ -1,5 +1,5 @@
 module Mixlib
   class ShellOut
-    VERSION = "1.3.0"
+    VERSION = "2.1.0"
   end
 end
diff --git a/lib/mixlib/shellout/windows.rb b/lib/mixlib/shellout/windows.rb
index ea809b5..1612b09 100644
--- a/lib/mixlib/shellout/windows.rb
+++ b/lib/mixlib/shellout/windows.rb
@@ -111,6 +111,12 @@ module Mixlib
               when WAIT_TIMEOUT
                 # Kill the process
                 if (Time.now - start_wait) > timeout
+                  begin
+                    Process.kill(:KILL, process.process_id)
+                  rescue Errno::EIO
+                    logger.warn("Failed to kill timed out process #{process.process_id}") if logger
+                  end
+
                   raise Mixlib::ShellOut::CommandTimeout, "command timed out:\n#{format_for_exception}"
                 end
 
@@ -122,8 +128,8 @@ module Mixlib
             end
 
           ensure
-            CloseHandle(process.thread_handle)
-            CloseHandle(process.process_handle)
+            CloseHandle(process.thread_handle) if process.thread_handle
+            CloseHandle(process.process_handle) if process.process_handle
           end
 
         ensure
@@ -156,7 +162,7 @@ module Mixlib
           begin
             next_chunk = stdout_read.readpartial(READ_SIZE)
             @stdout << next_chunk
-            @live_stream << next_chunk if @live_stream
+            @live_stdout << next_chunk if @live_stdout
           rescue EOFError
             stdout_read.close
             open_streams.delete(stdout_read)
@@ -165,7 +171,9 @@ module Mixlib
 
         if ready.first.include?(stderr_read)
           begin
-            @stderr << stderr_read.readpartial(READ_SIZE)
+            next_chunk = stderr_read.readpartial(READ_SIZE)
+            @stderr << next_chunk
+            @live_stderr << next_chunk if @live_stderr
           rescue EOFError
             stderr_read.close
             open_streams.delete(stderr_read)
@@ -198,7 +206,6 @@ module Mixlib
         end
       end
 
-
       # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
       # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
       # http://ss64.com/nt/syntax-esc.html
diff --git a/lib/mixlib/shellout/windows/core_ext.rb b/lib/mixlib/shellout/windows/core_ext.rb
index e692c7d..73b848f 100644
--- a/lib/mixlib/shellout/windows/core_ext.rb
+++ b/lib/mixlib/shellout/windows/core_ext.rb
@@ -288,19 +288,23 @@ module Process
 
         token = token.read_ulong
 
-        bool = CreateProcessAsUserW(
-          token,                  # User token handle
-          app,                    # App name
-          cmd,                    # Command line 
-          process_security,       # Process attributes
-          thread_security,        # Thread attributes
-          inherit,                # Inherit handles
-          hash['creation_flags'], # Creation Flags
-          env,                    # Environment
-          cwd,                    # Working directory
-          startinfo,              # Startup Info
-          procinfo                # Process Info
-        )
+        begin
+          bool = CreateProcessAsUserW(
+            token,                  # User token handle
+            app,                    # App name
+            cmd,                    # Command line
+            process_security,       # Process attributes
+            thread_security,        # Thread attributes
+            inherit,                # Inherit handles
+            hash['creation_flags'], # Creation Flags
+            env,                    # Environment
+            cwd,                    # Working directory
+            startinfo,              # Startup Info
+            procinfo                # Process Info
+          )
+        ensure
+          CloseHandle(token)
+        end
 
         unless bool
           raise SystemCallError.new("CreateProcessAsUserW (You must hold the 'Replace a process level token' permission)", FFI.errno)
@@ -346,9 +350,14 @@ module Process
     # Automatically close the process and thread handles in the
     # PROCESS_INFORMATION struct unless explicitly told not to.
     if hash['close_handles']
-      CloseHandle(procinfo[:hProcess])
-      CloseHandle(procinfo[:hThread])
-      CloseHandle(token)
+      CloseHandle(procinfo[:hProcess]) if procinfo[:hProcess]
+      CloseHandle(procinfo[:hThread]) if procinfo[:hThread]
+
+      # Set fields to nil so callers don't attempt to close the handle
+      # which can result in the wrong handle being closed or an
+      # exception in some circumstances
+      procinfo[:hProcess] = nil
+      procinfo[:hThread] = nil
     end
 
     ProcessInfo.new(
diff --git a/metadata.yml b/metadata.yml
index be43197..fe0b892 100644
--- a/metadata.yml
+++ b/metadata.yml
@@ -1,32 +1,29 @@
 --- !ruby/object:Gem::Specification
 name: mixlib-shellout
 version: !ruby/object:Gem::Version
-  version: 1.3.0
-  prerelease: 
+  version: 2.1.0
 platform: ruby
 authors:
 - Opscode
 autorequire: 
 bindir: bin
 cert_chain: []
-date: 2013-12-03 00:00:00.000000000 Z
+date: 2015-05-18 00:00:00.000000000 Z
 dependencies:
 - !ruby/object:Gem::Dependency
   name: rspec
   requirement: !ruby/object:Gem::Requirement
-    none: false
     requirements:
-    - - ~>
+    - - "~>"
       - !ruby/object:Gem::Version
-        version: '2.0'
+        version: '3.0'
   type: :development
   prerelease: false
   version_requirements: !ruby/object:Gem::Requirement
-    none: false
     requirements:
-    - - ~>
+    - - "~>"
       - !ruby/object:Gem::Version
-        version: '2.0'
+        version: '3.0'
 description: Run external commands on Unix or Windows
 email: info at opscode.com
 executables: []
@@ -37,34 +34,33 @@ extra_rdoc_files:
 files:
 - LICENSE
 - README.md
+- lib/mixlib/shellout.rb
 - lib/mixlib/shellout/exceptions.rb
 - lib/mixlib/shellout/unix.rb
 - lib/mixlib/shellout/version.rb
-- lib/mixlib/shellout/windows/core_ext.rb
 - lib/mixlib/shellout/windows.rb
-- lib/mixlib/shellout.rb
+- lib/mixlib/shellout/windows/core_ext.rb
 homepage: http://wiki.opscode.com/
 licenses: []
+metadata: {}
 post_install_message: 
 rdoc_options: []
 require_paths:
 - lib
 required_ruby_version: !ruby/object:Gem::Requirement
-  none: false
   requirements:
-  - - ! '>='
+  - - ">="
     - !ruby/object:Gem::Version
-      version: '0'
+      version: 1.9.3
 required_rubygems_version: !ruby/object:Gem::Requirement
-  none: false
   requirements:
-  - - ! '>='
+  - - ">="
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
 rubyforge_project: 
-rubygems_version: 1.8.23
+rubygems_version: 2.4.6
 signing_key: 
-specification_version: 3
+specification_version: 4
 summary: Run external commands on Unix or Windows
 test_files: []

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-mixlib-shellout.git



More information about the Pkg-ruby-extras-commits mailing list