require 'optparse'
require 'net/http'
require 'uri'

if RUBY_PLATFORM =~ /mswin32/ then abort("Reaper is only for Unix") end

# Instances of this class represent a single running process. Processes may
# be queried by "keyword" to find those that meet a specific set of criteria.
class ProgramProcess
  class << self
    
    # Searches for all processes matching the given keywords, and then invokes
    # a specific action on each of them. This is useful for (e.g.) reloading a
    # set of processes:
    #
    #   ProgramProcess.process_keywords(:reload, "basecamp")
    def process_keywords(action, *keywords)
      processes = keywords.collect { |keyword| find_by_keyword(keyword) }.flatten

      if processes.empty?
        puts "Couldn't find any process matching: #{keywords.join(" or ")}"
      else
        processes.each do |process|
          puts "#{action.capitalize}ing #{process}"
          process.send(action)
        end
      end      
    end

    # Searches for all processes matching the given keyword:
    #
    #   ProgramProcess.find_by_keyword("basecamp")
    def find_by_keyword(keyword)
      process_lines_with_keyword(keyword).split("\n").collect { |line|
        next if line =~ /inq|ps axww|grep|spawn-fcgi|spawner|reaper/
        pid, *command = line.split
        new(pid, command.join(" "))
      }.compact
    end

    private
      def process_lines_with_keyword(keyword)
        `ps axww -o 'pid command' | grep #{keyword}`
      end
  end

  # Create a new ProgramProcess instance that represents the process with the
  # given pid, running the given command.
  def initialize(pid, command)
    @pid, @command = pid, command
  end

  # Forces the (rails) application to reload by sending a +HUP+ signal to the
  # process.
  def reload
    `kill -s HUP #{@pid}`
  end

  # Forces the (rails) application to gracefully terminate by sending a
  # +TERM+ signal to the process.
  def graceful
    `kill -s TERM #{@pid}`
  end

  # Forces the (rails) application to terminate immediately by sending a -9
  # signal to the process.
  def kill
    `kill -9 #{@pid}`
  end

  # Send a +USR1+ signal to the process.
  def usr1
    `kill -s USR1 #{@pid}`
  end

  # Force the (rails) application to restart by sending a +USR2+ signal to the
  # process.
  def restart
    `kill -s USR2 #{@pid}`
  end

  def to_s #:nodoc:
    "[#{@pid}] #{@command}"
  end
end

OPTIONS = {
  :action      => "restart",
  :dispatcher  => File.expand_path(RAILS_ROOT + '/public/dispatch.fcgi')
}

ARGV.options do |opts|
  opts.banner = "Usage: reaper [options]"

  opts.separator ""

  opts.on <<-EOF
  Description:
    The reaper is used to restart, reload, gracefully exit, and forcefully exit FCGI processes
    running a Rails Dispatcher. This is commonly done when a new version of the application
    is available, so the existing processes can be updated to use the latest code.

    The reaper actions are:

    * restart : Restarts the application by reloading both application and framework code
    * reload  : Only reloads the application, but not the framework (like the development environment)
    * graceful: Marks all of the processes for exit after the next request
    * kill    : Forcefully exists all processes regardless of whether they're currently serving a request

    Restart is the most common and default action.

  Examples:
    reaper # restarts the default dispatcher
    reaper -a reload
    reaper -a exit -d /my/special/dispatcher.fcgi
  EOF

  opts.on("  Options:")

  opts.on("-a", "--action=name", "reload|graceful|kill (default: #{OPTIONS[:action]})", String)  { |OPTIONS[:action]| }
  opts.on("-d", "--dispatcher=path", "default: #{OPTIONS[:dispatcher]}", String)                 { |OPTIONS[:dispatcher]| }

  opts.separator ""

  opts.on("-h", "--help", "Show this help message.") { puts opts; exit }

  opts.parse!
end

ProgramProcess.process_keywords(OPTIONS[:action], OPTIONS[:dispatcher])
