def execute(cmd, options = {})
      options = {
        :directory => nil,
        :outstream => nil,
        :raise_on_failure => true,
        :set_env_vars => nil,
        :clear_env_vars => nil,
        :logger => default_logger,
        :timeout => nil,
        :keep_alive_interval => nil,
        :keep_alive_timeout => nil
      }.merge(options)

      outstream = options[:outstream]
      logger = options[:logger]

      if keep_alive_interval = options[:keep_alive_interval]
        keep_alive_wake_time = ::Time.now + keep_alive_interval
      else
        keep_alive_wake_time = nil
      end
      if keep_alive_timeout = options[:keep_alive_timeout]
        unless keep_alive_interval
          raise ::ArgumentError,
                ':keep_alive_interval is required when using :keep_alive_timeout'
        end
        keep_alive_stop_time = ::Time.now + keep_alive_timeout
      else
        keep_alive_stop_time = nil
      end

      # build initial popener.
      exitstatus = nil
      popener = lambda do |output|
        output.sync = true
        loop do
          # note stdout remains selectable after process dies.
          if (::IO.select([output], nil, nil, 0.1) rescue nil)
            if data = output.gets
              if outstream
                outstream << data
              else
                data = data.strip
                unless data.empty?
                  logger.info(data)

                  # reset keep alive timer whenever we have normal output.
                  if keep_alive_wake_time
                    keep_alive_wake_time = ::Time.now + keep_alive_interval
                  end
                end
              end
            else
              break
            end
          elsif keep_alive_wake_time
            now = ::Time.now
            if keep_alive_stop_time && now >= keep_alive_stop_time
              keep_alive_wake_time = nil
            elsif now >= keep_alive_wake_time
              # keep-alives go to logger, not the outstream, if any.
              logger.info('.')
              keep_alive_wake_time = now + keep_alive_interval
            end
            now = nil
          end
        end
      end

      # timeout optionally wraps popener. the timeout must happen inside of the
      # IO.popen block or else it has no good effect.
      if timeout = options[:timeout]
        popener = lambda do |p|
          lambda do |o|
            ::Timeout.timeout(timeout) { p.call(o) }
          end
        end.call(popener)
      end

      # build initial executioner in terms of popener.
      executioner = lambda do
        logger.info("+ #{cmd}")
        error_msg = nil
        ::IO.popen("#{cmd} 2>&1", 'r') do |output|
          begin
            popener.call(output)
          rescue ::EOFError
            # done
          rescue ::Timeout::Error
            # kill still-running process or else popen's ensure will hang.
            ::Process.kill('KILL', output.pid)

            # intentionally not reading last data as that could still block
            # due to a child of created process inheriting stdout.
            error_msg = "Execution timed out after #{options[:timeout]} seconds."
          end
        end

        # note that a killed process may exit 0 under Windows.
        exitstatus = $?.exitstatus
        if 0 == exitstatus && error_msg
          exitstatus = 1
        end
        if (exitstatus != 0 && options[:raise_on_failure])
          error_msg ||= "Execution failed with exitstatus #{exitstatus}"
          raise ShellError, error_msg
        end
      end

      # configure executioner (by options) and then invoke executioner.
      configure_executioner(executioner, options).call

      return exitstatus
    end
def execute(cmd, options = {})
      options = {
        :directory => nil,
        :outstream => nil,
        :raise_on_failure => true,
        :set_env_vars => nil,
        :clear_env_vars => nil,
        :logger => default_logger,
        :timeout => nil,
        :keep_alive_interval => nil,
        :keep_alive_timeout => nil
      }.merge(options)

      outstream = options[:outstream]
      logger = options[:logger]

      if keep_alive_interval = options[:keep_alive_interval]
        keep_alive_wake_time = ::Time.now + keep_alive_interval
      else
        keep_alive_wake_time = nil
      end
      if keep_alive_timeout = options[:keep_alive_timeout]
        unless keep_alive_interval
          raise ::ArgumentError,
                ':keep_alive_interval is required when using :keep_alive_timeout'
        end
        keep_alive_stop_time = ::Time.now + keep_alive_timeout
      else
        keep_alive_stop_time = nil
      end

      # build initial popener.
      exitstatus = nil
      popener = lambda do |output|
        output.sync = true
        loop do
          # note stdout remains selectable after process dies.
          if (::IO.select([output], nil, nil, 0.1) rescue nil)
            if data = output.gets
              if outstream
                outstream << data
              else
                data = data.strip
                unless data.empty?
                  logger.info(data)

                  # reset keep alive timer whenever we have normal output.
                  if keep_alive_wake_time
                    keep_alive_wake_time = ::Time.now + keep_alive_interval
                  end
                end
              end
            else
              break
            end
          elsif keep_alive_wake_time
            now = ::Time.now
            if keep_alive_stop_time && now >= keep_alive_stop_time
              keep_alive_wake_time = nil
            elsif now >= keep_alive_wake_time
              # keep-alives go to logger, not the outstream, if any.
              logger.info('.')
              keep_alive_wake_time = now + keep_alive_interval
            end
            now = nil
          end
        end
      end

      # timeout optionally wraps popener. the timeout must happen inside of the
      # IO.popen block or else it has no good effect.
      if timeout = options[:timeout]
        popener = lambda do |p|
          lambda do |o|
            ::Timeout.timeout(timeout) { p.call(o) }
          end
        end.call(popener)
      end

      # build initial executioner in terms of popener.
      executioner = lambda do
        logger.info("+ #{cmd}")
        error_msg = nil
        ::IO.popen("#{cmd} 2>&1", 'r') do |output|
          begin
            popener.call(output)
          rescue ::EOFError
            # done
          rescue ::Timeout::Error
            # kill still-running process or else popen's ensure will hang.
            ::Process.kill('KILL', output.pid)

            # intentionally not reading last data as that could still block
            # due to a child of created process inheriting stdout.
            error_msg = "Execution timed out after #{options[:timeout]} seconds."
          end
        end

        # note that a killed process may exit 0 under Windows.
        exitstatus = $?.exitstatus
        if 0 == exitstatus && error_msg
          exitstatus = 1
        end
        if (exitstatus != 0 && options[:raise_on_failure])
          error_msg ||= "Execution failed with exitstatus #{exitstatus}"
          raise ShellError, error_msg
        end
      end

      # configure executioner (by options) and then invoke executioner.
      configure_executioner(executioner, options).call

      return exitstatus
    end
Implements execute interface.