
module Sprout

  # The ProcessRunner is a cross-platform wrapper for executing
  # external executable processes.
  #
  # This class is typically accesses via the concrete Sprout::System
  # classes in order to avoid ugly branching logic in the application
  # layer.
  #
  # An example of this kind of usage might be:
  #
  #   Sprout.current_system.execute './some.exe', '--foo=bar --baz=buz'
  #
  # To use this class directly, you need to know if you're on
  # a unix-like system or a dos platform as these two deal with
  # processes very differently.
  #
  # Assuming you know you're on a unix-like system, you could
  # execute the previous example with:
  #
  #   runner = Sprout::ProcessRunner.new
  #   runner.execute_open4 './some.exe --foo-bar --baz=buz'
  #   puts runner.read
  #
  class ProcessRunner

    attr_reader :pid
    attr_reader :ruby_version

    ##
    # @return [IO] Read only
    attr_reader :r

    ##
    # @return [IO] Write only
    attr_reader :w

    ##
    # @return [IO] Error output
    attr_reader :e

    def initialize
      super
      @ruby_version = RUBY_VERSION
    end
    
    ##
    # Execute the provided command using the open4.popen4
    # library. This is generally only used by Cygwin and
    # *nix variants (including OS X).
    def execute_open4 *command
      execute_with_block *command do
        # Not sure about the join - with the 1.0 push, we're
        # sending in 2 args - the exe path, and options as a string.
        # This was new and was causing problems...
        @pid, @w, @r, @e = open4_popen4_block *command.join(' ')
      end
    end
    
    ##
    # Execute the provided command using the win32-open3
    # library. This should only be used on Windows
    # systems (even 64 bit).
    def execute_win32(*command)
      execute_with_block *command do
        @pid, @w, @r, @e = io_popen_block *command.join(' ')
      end
    end

    ##
    # @return [Boolean] whether the process is still running.
    def alive?
      @alive = update_status
    end
    
    ##
    # Kill the process.
    def kill
      update_status 9
    end
    
    ##
    # Close the process
    def close
      w.close_write
    end
    
    ##
    # Send an update signal to the process.
    #
    # @param sig [Integer] The signal to send, default 0 (or no action requested)
    def update_status sig=0
      pid_int = Integer("#{ @pid }")
      begin
        Process::kill sig, pid_int
        true
      rescue Errno::ESRCH
        false
      end
    end
    
    ##
    # Read +count+ characters from the process standard out.
    #
    # @param count [Integer] Number of characters to read.
    # @return [String]
    def readpartial count
      @r.readpartial count
    end
    
    ##
    # Read +count+ lines from the process standard out.
    #
    # @param count [Integer] Number of lines to read.
    # @return [String]
    def readlines count
      @r.readlines count
    end
    
    ##
    # Flush the write IO to the process.
    def flush
      @w.flush
    end
    
    ##
    # Get user input on the read stream from the process.
    def getc
      @r.getc
    end
    
    ##
    # Print some characters to process without an end of line character.
    def print msg
      @w.print msg
    end
    
    ##
    # Print characters to the process followed by an end of line.
    def puts(msg)
      @w.puts(msg)
    end
    
    ##
    # Close the write stream - usually terminates the process.
    def close_write
      @w.close_write
    end
    
    ##
    # Wait for the process to end and return the entire standard output.
    def read
      return @r.read
    end
    
    ##
    # Wait for the process to end and return the entire standard error.
    def read_err
      return @e.read
    end

    def readpartial_err count
      return @e.readpartial count
    end

    private

    def open4_popen4_block *command
      require 'open4'
      open4.popen4(*command)
    end

    def io_popen_block *command
      if(ruby_version == '1.8.6' || ruby_version == '1.8.7')
        win32_open3_block *command
      else
        open3_popen3_block *command
      end
    end

    def open3_popen3_block *command
      require 'open3'
      write, read, error, wait_thread = Open3.popen3(*command)
      [wait_thread[:pid], write, read, error]
    end

    def win32_open3_block *command
      write, read, error, pid = nil
      gem 'win32-open3'
      require 'win32/open3'
      Open3.popen3(*command) do |w, r, e, p|
            pid   = p
            write = w
            read  = r
            error = e
      end
      [pid, write, read, error]
    end

    def execute_with_block *command
      begin
        @alive = true
        yield 
      rescue Errno::EACCES => eacces
        update_executable_mode(*command)
        yield
      rescue Errno::ENOENT => enoent
        @alive = false
        part = command[0]
        raise Sprout::Errors::ProcessRunnerError.new("The expected executable was not found for command [#{part}], please check your system path and/or sprout definition")
      end
    end

    def update_executable_mode(*command)
      str = command.shift
      if(File.exists?(str))
        FileUtils.chmod(0744, str)
      end
    end
    
  end
end

