# Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html # for more information. require 'mongrel' require 'cgi' module Mongrel module Rails # Implements a handler that can run Rails and serve files out of the # Rails application's public directory. This lets you run your Rails # application with Mongrel during development and testing, then use it # also in production behind a server that's better at serving the # static files. # # The RailsHandler takes a mime_map parameter which is a simple suffix=mimetype # mapping that it should add to the list of valid mime types. # # It also supports page caching directly and will try to resolve a request # in the following order: # # * If the requested exact PATH_INFO exists as a file then serve it. # * If it exists at PATH_INFO+".html" exists then serve that. # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispatch to have Rails go. # # This means that if you are using page caching it will actually work with Mongrel # and you should see a decent speed boost (but not as fast as if you use a static # server like Apache or Litespeed). class RailsHandler < Mongrel::HttpHandler attr_reader :files attr_reader :guard @@file_only_methods = ["GET","HEAD"] def initialize(dir, mime_map = {}) @files = Mongrel::DirHandler.new(dir,false) @guard = Mutex.new # Register the requested MIME types mime_map.each {|k,v| Mongrel::DirHandler::add_mime_type(k,v) } end # Attempts to resolve the request as follows: # # * If the requested exact PATH_INFO exists as a file then serve it. # * If it exists at PATH_INFO+".html" exists then serve that. # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispatch to have Rails go. def process(request, response) return if response.socket.closed? path_info = request.params[Mongrel::Const::PATH_INFO] rest_operator = request.params[Mongrel::Const::REQUEST_URI][/^#{Regexp.escape path_info}(;[^\?]+)/, 1].to_s path_info.chomp!("/") page_cached = path_info + rest_operator + ActionController::Base.page_cache_extension get_or_head = @@file_only_methods.include? request.params[Mongrel::Const::REQUEST_METHOD] if get_or_head and @files.can_serve(path_info) # File exists as-is so serve it up @files.process(request,response) elsif get_or_head and @files.can_serve(page_cached) # Possible cached page, serve it up request.params[Mongrel::Const::PATH_INFO] = page_cached @files.process(request,response) else begin cgi = Mongrel::CGIWrapper.new(request, response) cgi.handler = self # We don't want the output to be really final until we're out of the lock cgi.default_really_final = false @guard.synchronize { @active_request_path = request.params[Mongrel::Const::PATH_INFO] Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body) @active_request_path = nil } # This finalizes the output using the proper HttpResponse way cgi.out("text/html",true) {""} rescue Errno::EPIPE response.socket.close rescue Object => rails_error STDERR.puts "#{Time.now}: Error calling Dispatcher.dispatch #{rails_error.inspect}" STDERR.puts rails_error.backtrace.join("\n") end end end # Does the internal reload for Rails. It might work for most cases, but # sometimes you get exceptions. In that case just do a real restart. def reload! begin @guard.synchronize { $".replace $orig_dollar_quote GC.start Dispatcher.reset_application! ActionController::Routing::Routes.reload } end end end # Creates Rails specific configuration options for people to use # instead of the base Configurator. class RailsConfigurator < Mongrel::Configurator # Creates a single rails handler and returns it so you # can add it to a URI. You can actually attach it to # as many URIs as you want, but this returns the # same RailsHandler for each call. # # Requires the following options: # # * :docroot => The public dir to serve from. # * :environment => Rails environment to use. # * :cwd => The change to working directory # # And understands the following optional settings: # # * :mime => A map of mime types. # # Because of how Rails is designed you can only have # one installed per Ruby interpreter (talk to them # about thread safety). Because of this the first # time you call this function it does all the config # needed to get your Rails working. After that # it returns the one handler you've configured. # This lets you attach Rails to any URI(s) you want, # but it still protects you from threads destroying # your handler. def rails(options={}) return @rails_handler if @rails_handler ops = resolve_defaults(options) # fix up some defaults ops[:environment] ||= "development" ops[:docroot] ||= "public" ops[:mime] ||= {} $orig_dollar_quote = $".clone ENV['RAILS_ENV'] = ops[:environment] env_location = "#{ops[:cwd]}/config/environment" require env_location require 'dispatcher' require 'mongrel/rails' ActionController::AbstractRequest.relative_url_root = ops[:prefix] if ops[:prefix] @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime]) end # Reloads Rails. This isn't too reliable really, but it # should work for most minimal reload purposes. The only reliable # way to reload properly is to stop and then start the process. def reload! if not @rails_handler raise "Rails was not configured. Read the docs for RailsConfigurator." end log "Reloading Rails..." @rails_handler.reload! log "Done reloading Rails." end # Takes the exact same configuration as Mongrel::Configurator (and actually calls that) # but sets up the additional HUP handler to call reload!. def setup_rails_signals(options={}) ops = resolve_defaults(options) setup_signals(options) unless RbConfig::CONFIG['host_os'] =~ /mingw|mswin/ # rails reload trap("HUP") { log "HUP signal received."; reload! } log "Rails signals registered. HUP => reload (without restart). It might not work well." end end end end end