require 'yaml'
require 'etc'


module Mongrel
  # Implements a simple DSL for configuring a Mongrel server for your 
  # purposes.  More used by framework implementers to setup Mongrel
  # how they like, but could be used by regular folks to add more things
  # to an existing mongrel configuration.
  #
  # It is used like this:
  #
  #   require 'mongrel'
  #   config = Mongrel::Configurator.new :host => "127.0.0.1" do
  #     listener :port => 3000 do
  #       uri "/app", :handler => Mongrel::DirHandler.new(".", load_mime_map("mime.yaml"))
  #     end
  #     run
  #   end
  # 
  # This will setup a simple DirHandler at the current directory and load additional
  # mime types from mimy.yaml.  The :host => "127.0.0.1" is actually not 
  # specific to the servers but just a hash of default parameters that all 
  # server or uri calls receive.
  #
  # When you are inside the block after Mongrel::Configurator.new you can simply
  # call functions that are part of Configurator (like server, uri, daemonize, etc)
  # without having to refer to anything else.  You can also call these functions on 
  # the resulting object directly for additional configuration.
  #
  # A major thing about Configurator is that it actually lets you configure 
  # multiple listeners for any hosts and ports you want.  These are kept in a
  # map config.listeners so you can get to them.
  #
  # * :pid_file => Where to write the process ID.
  class Configurator
    attr_reader :listeners
    attr_reader :defaults
    attr_reader :needs_restart

    # You pass in initial defaults and then a block to continue configuring.
    def initialize(defaults={}, &blk)
      @listener = nil
      @listener_name = nil
      @listeners = {}
      @defaults = defaults
      @needs_restart = false
      @pid_file = defaults[:pid_file]

      if blk
        cloaker(&blk).bind(self).call
      end
    end

    # Change privilege of the process to specified user and group.
    def change_privilege(user, group)
      begin
        if group
          log "Changing group to #{group}."
          Process::GID.change_privilege(Etc.getgrnam(group).gid)
        end

        if user
          log "Changing user to #{user}." 
          Process::UID.change_privilege(Etc.getpwnam(user).uid)
        end
      rescue Errno::EPERM
        log "FAILED to change user:group #{user}:#{group}: #$!"
        exit 1
      end
    end

    def remove_pid_file
      File.unlink(@pid_file) if @pid_file and File.exists?(@pid_file)
    end

    # Writes the PID file but only if we're on windows.
    def write_pid_file
      if RUBY_PLATFORM !~ /mswin/
        log "Writing PID file to #{@pid_file}"
        open(@pid_file,"w") {|f| f.write(Process.pid) }
      end
    end

    # generates a class for cloaking the current self and making the DSL nicer
    def cloaking_class
      class << self
        self
      end
    end

    # Do not call this.  You were warned.
    def cloaker(&blk)
      cloaking_class.class_eval do
        define_method :cloaker_, &blk
        meth = instance_method( :cloaker_ )
        remove_method :cloaker_
        meth
      end
    end

    # This will resolve the given options against the defaults.
    # Normally just used internally.
    def resolve_defaults(options)
      options.merge(@defaults)
    end

    # Starts a listener block.  This is the only one that actually takes
    # a block and then you make Configurator.uri calls in order to setup
    # your URIs and handlers.  If you write your Handlers as GemPlugins
    # then you can use load_plugins and plugin to load them.
    # 
    # It expects the following options (or defaults):
    # 
    # * :host => Host name to bind.
    # * :port => Port to bind.
    # * :num_processors => The maximum number of concurrent threads allowed.  (950 default)
    # * :timeout => 1/100th of a second timeout between requests. (10 is 1/10th, 0 is timeout)
    # * :user => User to change to, must have :group as well.
    # * :group => Group to change to, must have :user as well.
    #
    def listener(options={},&blk)
      raise "Cannot call listener inside another listener block." if (@listener or @listener_name)
      ops = resolve_defaults(options)
      ops[:num_processors] ||= 950
      ops[:timeout] ||= 0

      @listener = Mongrel::HttpServer.new(ops[:host], ops[:port].to_i, ops[:num_processors].to_i, ops[:timeout].to_i)
      @listener_name = "#{ops[:host]}:#{ops[:port]}"
      @listeners[@listener_name] = @listener

      if ops[:user] and ops[:group]
        change_privilege(ops[:user], ops[:group])
      end

      # Does the actual cloaking operation to give the new implicit self.
      if blk
        cloaker(&blk).bind(self).call
      end

      # all done processing this listener setup, reset implicit variables
      @listener = nil
      @listener_name = nil
    end


    # Called inside a Configurator.listener block in order to 
    # add URI->handler mappings for that listener.  Use this as
    # many times as you like.  It expects the following options
    # or defaults:
    #
    # * :handler => HttpHandler -- Handler to use for this location.
    # * :in_front => true/false -- Rather than appending, it prepends this handler.
    def uri(location, options={})
      ops = resolve_defaults(options)
      @listener.register(location, ops[:handler], ops[:in_front])
    end


    # Daemonizes the current Ruby script turning all the
    # listeners into an actual "server" or detached process.
    # You must call this *before* frameworks that open files
    # as otherwise the files will be closed by this function.
    #
    # Does not work for Win32 systems (the call is silently ignored).
    #
    # Requires the following options or defaults:
    #
    # * :cwd => Directory to change to.
    # * :log_file => Where to write STDOUT and STDERR.
    # 
    # It is safe to call this on win32 as it will only require the daemons
    # gem/library if NOT win32.
    def daemonize(options={})
      ops = resolve_defaults(options)
      # save this for later since daemonize will hose it
      if RUBY_PLATFORM !~ /mswin/
        require 'daemons/daemonize'

        logfile = ops[:log_file]
        if logfile[0].chr != "/"
          logfile = File.join(ops[:cwd],logfile)
          if not File.exist?(File.dirname(logfile))
            log "!!! Log file directory not found at full path #{File.dirname(logfile)}.  Update your configuration to use a full path."
            exit 1
          end
        end

        Daemonize.daemonize(logfile)

        # change back to the original starting directory
        Dir.chdir(ops[:cwd])

      else
        log "WARNING: Win32 does not support daemon mode."
      end
    end


    # Uses the GemPlugin system to easily load plugins based on their
    # gem dependencies.  You pass in either an :includes => [] or 
    # :excludes => [] setting listing the names of plugins to include
    # or exclude from the when determining the dependencies.
    def load_plugins(options={})
      ops = resolve_defaults(options)

      load_settings = {}
      if ops[:includes]
        ops[:includes].each do |plugin|
          load_settings[plugin] = GemPlugin::INCLUDE
        end
      end

      if ops[:excludes]
        ops[:excludes].each do |plugin|
          load_settings[plugin] = GemPlugin::EXCLUDE
        end
      end

      GemPlugin::Manager.instance.load(load_settings)
    end


    # Easy way to load a YAML file and apply default settings.
    def load_yaml(file, default={})
      default.merge(YAML.load_file(file))
    end


    # Loads the MIME map file and checks that it is correct
    # on loading.  This is commonly passed to Mongrel::DirHandler
    # or any framework handler that uses DirHandler to serve files.
    # You can also include a set of default MIME types as additional
    # settings.  See Mongrel::DirHandler for how the MIME types map
    # is organized.
    def load_mime_map(file, mime={})
      # configure any requested mime map
      mime = load_yaml(file, mime)

      # check all the mime types to make sure they are the right format
      mime.each {|k,v| log "WARNING: MIME type #{k} must start with '.'" if k.index(".") != 0 }

      return mime
    end


    # Loads and creates a plugin for you based on the given
    # name and configured with the selected options.  The options
    # are merged with the defaults prior to passing them in.
    def plugin(name, options={})
      ops = resolve_defaults(options)
      GemPlugin::Manager.instance.create(name, ops)
    end

    # Lets you do redirects easily as described in Mongrel::RedirectHandler.
    # You use it inside the configurator like this:
    #
    #   redirect("/test", "/to/there") # simple
    #   redirect("/to", /t/, 'w') # regexp
    #   redirect("/hey", /(w+)/) {|match| ...}  # block
    #
    def redirect(from, pattern, replacement = nil, &block)
      uri from, :handler => Mongrel::RedirectHandler.new(pattern, replacement, &block)
    end

    # Works like a meta run method which goes through all the 
    # configured listeners.  Use the Configurator.join method
    # to prevent Ruby from exiting until each one is done.
    def run
      @listeners.each {|name,s| 
        s.run 
      }

      $mongrel_sleeper_thread = Thread.new { loop { sleep 1 } }
    end

    # Calls .stop on all the configured listeners so they
    # stop processing requests (gracefully).  By default it
    # assumes that you don't want to restart.
    def stop(needs_restart=false)
      @listeners.each {|name,s| 
        s.stop 
      }

      @needs_restart = needs_restart
    end


    # This method should actually be called *outside* of the
    # Configurator block so that you can control it.  In other words
    # do it like:  config.join.
    def join
      @listeners.values.each {|s| s.acceptor.join }
    end


    # Calling this before you register your URIs to the given location
    # will setup a set of handlers that log open files, objects, and the
    # parameters for each request.  This helps you track common problems
    # found in Rails applications that are either slow or become unresponsive
    # after a little while.
    #
    # You can pass an extra parameter *what* to indicate what you want to 
    # debug.  For example, if you just want to dump rails stuff then do:
    #
    #   debug "/", what = [:rails]
    # 
    # And it will only produce the log/mongrel_debug/rails.log file.
    # Available options are: :access, :files, :objects, :threads, :rails 
    # 
    # NOTE: Use [:files] to get accesses dumped to stderr like with WEBrick.
    def debug(location, what = [:access, :files, :objects, :threads, :rails])
      require 'mongrel/debug'
      handlers = {
        :access => "/handlers/requestlog::access", 
        :files => "/handlers/requestlog::files", 
        :objects => "/handlers/requestlog::objects", 
        :threads => "/handlers/requestlog::threads",
        :rails => "/handlers/requestlog::params"
      }

      # turn on the debugging infrastructure, and ObjectTracker is a pig
      MongrelDbg.configure

      # now we roll through each requested debug type, turn it on and load that plugin
      what.each do |type| 
        MongrelDbg.begin_trace type 
        uri location, :handler => plugin(handlers[type])
      end
    end

    # Used to allow you to let users specify their own configurations
    # inside your Configurator setup.  You pass it a script name and
    # it reads it in and does an eval on the contents passing in the right
    # binding so they can put their own Configurator statements.
    def run_config(script)
      open(script) {|f| eval(f.read, proc {self}) }
    end

    # Sets up the standard signal handlers that are used on most Ruby
    # It only configures if the platform is not win32 and doesn't do
    # a HUP signal since this is typically framework specific.
    #
    # Requires a :pid_file option given to Configurator.new to indicate a file to delete.  
    # It sets the MongrelConfig.needs_restart attribute if 
    # the start command should reload.  It's up to you to detect this
    # and do whatever is needed for a "restart".
    #
    # This command is safely ignored if the platform is win32 (with a warning)
    def setup_signals(options={})
      ops = resolve_defaults(options)

      # forced shutdown, even if previously restarted (actually just like TERM but for CTRL-C)
      trap("INT") { log "INT signal received."; stop(false) }

      # clean up the pid file always
      at_exit { remove_pid_file }

      if RUBY_PLATFORM !~ /mswin/
        # graceful shutdown
        trap("TERM") { log "TERM signal received."; stop }
        trap("USR1") { log "USR1 received, toggling $mongrel_debug_client to #{!$mongrel_debug_client}"; $mongrel_debug_client = !$mongrel_debug_client }
        # restart
        trap("USR2") { log "USR2 signal received."; stop(true) }

        log "Signals ready.  TERM => stop.  USR2 => restart.  INT => stop (no restart)."
      else
        log "Signals ready.  INT => stop (no restart)."
      end
    end

    # Logs a simple message to STDERR (or the mongrel log if in daemon mode).
    def log(msg)
      STDERR.print "** ", msg, "\n"
    end

  end
end
