require "thread"
require "socket"
require "shellwords"

module Command
  class Error < StandardError
    attr_reader :command, :result
    def initialize(command, result)
      @command = command
      @result = result
      super("#{command}: #{result}")
    end
  end

  module_function
  def detach_io
    require 'fcntl'
    [TCPSocket, ::File].each do |c|
      ObjectSpace.each_object(c) do |io|
        begin
          unless io.closed?
            io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
          end
        rescue SystemCallError, IOError
        end
      end
    end
  end

  def run(cmd, *args, &block)
    raise ArgumentError, "command isn't specified" if cmd.nil?
    if args.any? {|x| x.nil?}
      raise ArgumentError, "args has nil: #{args.inspect}"
    end
    return java_run(cmd, *args, &block) if Object.respond_to?(:java)
    in_r, in_w = IO.pipe
    out_r, out_w = IO.pipe
    pid = nil
    Thread.exclusive do
      verbose = $VERBOSE
      # ruby(>=1.8)'s fork terminates other threads with warning messages
      $VERBOSE = nil
      pid = fork do
        $VERBOSE = verbose
        detach_io
        STDIN.reopen(in_r)
        in_r.close
        STDOUT.reopen(out_w)
        STDERR.reopen(out_w)
        out_w.close
        exec(cmd, *args.collect {|arg| arg.to_s})
        exit!(-1)
      end
      $VERBOSE = verbose
    end
    yield(out_r, in_w) if block_given?
    in_r.close unless in_r.closed?
    out_w.close unless out_w.closed?
    pid, status = Process.waitpid2(pid)
    [status.exited? && status.exitstatus.zero?, out_r.read]
  end

  def java_run(cmd, *args, &block)
    runtime = java.lang.Runtime.get_runtime
    process = runtime.exec([cmd, *args].to_java(:string))
    input = JavaReaderWrapper.new(process.get_input_stream)
    output = JavaWriterWrapper.new(process.get_output_stream)
    error = JavaReaderWrapper.new(process.get_error_stream)
    yield(input, output) if block_given?
    output.close
    success = process.wait_for.zero?

    [success, input.read + error.read]
  end

  class JavaReaderWrapper
    def initialize(input)
      @input = input
    end

    def read
      result = ""
      while (c = @input.read) != -1
        result << c.chr
      end
      result
    end
  end

  class JavaWriterWrapper
    def initialize(output)
      output = java.io.OutputStreamWriter.new(output)
      @output = java.io.BufferedWriter.new(output)
    end

    def puts(*messages)
      messages.each do |message|
        message += "\n" if /\n/ !~ message
        @output.write(message)
      end
    end

    def flush
      @output.flush
    end

    def close
      @output.close
    end
  end
end
