about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-07-13 22:34:59 +0000
committerzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-07-13 22:34:59 +0000
commitab3c8082de82e6fc96838d444be06432620743ab (patch)
tree4b6324ed627ff4083a61f5c011500ee41893e238
parenta24136cd55995b748e3fed0db382f20cd49a315e (diff)
downloadunicorn-ab3c8082de82e6fc96838d444be06432620743ab.tar.gz
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@292 19e92222-5c0b-0410-8929-a290d50e31e9
-rw-r--r--Rakefile2
-rw-r--r--bin/mongrel_rails12
-rw-r--r--ext/http11/http11.c101
-rw-r--r--lib/mongrel.rb442
-rw-r--r--lib/mongrel/configurator.rb366
-rw-r--r--lib/mongrel/rails.rb6
-rw-r--r--test/test_ws.rb4
7 files changed, 492 insertions, 441 deletions
diff --git a/Rakefile b/Rakefile
index db4a7c3..cab1fc2 100644
--- a/Rakefile
+++ b/Rakefile
@@ -53,7 +53,7 @@ task :site => [:site_webgen, :site_rdoc, :site_coverage, :site_projects_rdoc]
 setup_extension("http11", "http11")
 
 name="mongrel"
-version="0.3.13.3"
+version="0.3.13.4"
 
 setup_gem(name, version) do |spec|
   spec.summary = "A small fast HTTP library and server that runs Rails, Camping, and Nitro apps."
diff --git a/bin/mongrel_rails b/bin/mongrel_rails
index f404044..8dd59e9 100644
--- a/bin/mongrel_rails
+++ b/bin/mongrel_rails
@@ -32,7 +32,8 @@ class Start < GemPlugin::Plugin "/commands"
       ['-S', '--script PATH', "Load the given file as an extra config script.", :@config_script, nil],
       ['-G', '--generate CONFIG', "Generate a config file for -C", :@generate, nil],
       ['', '--user USER', "User to run as", :@user, nil],
-      ['', '--group GROUP', "Group to run as", :@group, nil]
+      ['', '--group GROUP', "Group to run as", :@group, nil],
+      ['', '--prefix PATH', "URL prefix for Rails app", :@prefix, '/']
     ]
   end
 
@@ -64,7 +65,7 @@ class Start < GemPlugin::Plugin "/commands"
       :docroot => @docroot, :mime_map => @mime_map, :daemon => @daemon,
       :debug => @debug, :includes => ["mongrel"], :config_script => @config_script,
       :num_processors => @num_procs, :timeout => @timeout,
-      :user => @user, :group => @group
+      :user => @user, :group => @group, :prefix => @prefix
     }
 
     if @generate
@@ -103,13 +104,10 @@ class Start < GemPlugin::Plugin "/commands"
         if defaults[:debug]
           log "Installing debugging prefixed filters.  Look in log/mongrel_debug for the files."
           debug "/"
-        elsif not defaults[:daemon]
-          # they don't have debug on and aren't in daemon so at least log accesses
-          debug "/", what = [:files]
         end
 
-        log "Starting Rails with #{defaults[:environment]} environment ..."
-        uri "/", :handler => rails(:mime => mime)
+        log "Starting Rails with #{defaults[:environment]} environment on #{defaults[:prefix]} ..."
+        uri defaults[:prefix], :handler => rails(:mime => mime, :prefix => @prefix)
         log "Rails loaded."
 
         log "Loading any Rails specific GemPlugins"
diff --git a/ext/http11/http11.c b/ext/http11/http11.c
index a4a025d..d69e347 100644
--- a/ext/http11/http11.c
+++ b/ext/http11/http11.c
@@ -14,7 +14,9 @@ static VALUE mMongrel;
 static VALUE cHttpParser;
 static VALUE cURIClassifier;
 static VALUE eHttpParserError;
-static ID id_handler_map;
+
+#define id_handler_map rb_intern("@handler_map")
+#define id_http_body rb_intern("@http_body")
 
 static VALUE global_http_prefix;
 static VALUE global_request_method;
@@ -121,7 +123,7 @@ void http_version(void *data, const char *at, size_t length)
 }
 
 /** Finalizes the request header to have a bunch of stuff that's
-    needed. */
+  needed. */
 
 void header_done(void *data, const char *at, size_t length)
 {
@@ -148,44 +150,46 @@ void header_done(void *data, const char *at, size_t length)
     if(colon != NULL) {
       rb_hash_aset(req, global_server_name, rb_str_substr(temp, 0, colon - RSTRING(temp)->ptr));
       rb_hash_aset(req, global_server_port,
-                   rb_str_substr(temp, colon - RSTRING(temp)->ptr+1,
-                                 RSTRING(temp)->len));
+          rb_str_substr(temp, colon - RSTRING(temp)->ptr+1,
+            RSTRING(temp)->len));
     } else {
       rb_hash_aset(req, global_server_name, temp);
       rb_hash_aset(req, global_server_port, global_port_80);
     }
   }
-  
+
+  // grab the initial body and stuff it into an ivar
+  rb_ivar_set(req, id_http_body, rb_str_new(at, length));
   rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
   rb_hash_aset(req, global_server_software, global_mongrel_version);
 }
 
 
 void HttpParser_free(void *data) {
-    TRACE();
-    
-    if(data) {
-        free(data);
-    }
+  TRACE();
+
+  if(data) {
+    free(data);
+  }
 }
 
 
 VALUE HttpParser_alloc(VALUE klass)
 {
-    VALUE obj;
-    http_parser *hp = ALLOC_N(http_parser, 1);
-    TRACE();
-    hp->http_field = http_field;
-    hp->request_method = request_method;
-    hp->request_uri = request_uri;
-    hp->query_string = query_string;
-    hp->http_version = http_version;
-    hp->header_done = header_done;
-    http_parser_init(hp);
-
-    obj = Data_Wrap_Struct(klass, NULL, HttpParser_free, hp);
-
-    return obj;
+  VALUE obj;
+  http_parser *hp = ALLOC_N(http_parser, 1);
+  TRACE();
+  hp->http_field = http_field;
+  hp->request_method = request_method;
+  hp->request_uri = request_uri;
+  hp->query_string = query_string;
+  hp->http_version = http_version;
+  hp->header_done = header_done;
+  http_parser_init(hp);
+
+  obj = Data_Wrap_Struct(klass, NULL, HttpParser_free, hp);
+
+  return obj;
 }
 
 
@@ -200,7 +204,7 @@ VALUE HttpParser_init(VALUE self)
   http_parser *http = NULL;
   DATA_GET(self, http_parser, http);
   http_parser_init(http);
-  
+
   return self;
 }
 
@@ -217,7 +221,7 @@ VALUE HttpParser_reset(VALUE self)
   http_parser *http = NULL;
   DATA_GET(self, http_parser, http);
   http_parser_init(http);
-  
+
   return Qnil;
 }
 
@@ -234,7 +238,7 @@ VALUE HttpParser_finish(VALUE self)
   http_parser *http = NULL;
   DATA_GET(self, http_parser, http);
   http_parser_finish(http);
-  
+
   return http_parser_is_finished(http) ? Qtrue : Qfalse;
 }
 
@@ -268,15 +272,15 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
   from = FIX2INT(start);
   dptr = RSTRING(data)->ptr;
   dlen = RSTRING(data)->len;
-  
+
   if(from >= dlen) {
     rb_raise(eHttpParserError, "Requested start is after data buffer end.");
   } else {
     http->data = (void *)req_hash;
     http_parser_execute(http, dptr, dlen, from);
-    
+
     VALIDATE_MAX_LENGTH(http_parser_nread(http), HEADER);
-    
+
     if(http_parser_has_error(http)) {
       rb_raise(eHttpParserError, "Invalid HTTP format, parsing fails.");
     } else {
@@ -297,7 +301,7 @@ VALUE HttpParser_has_error(VALUE self)
 {
   http_parser *http = NULL;
   DATA_GET(self, http_parser, http);
-  
+
   return http_parser_has_error(http) ? Qtrue : Qfalse;
 }
 
@@ -312,7 +316,7 @@ VALUE HttpParser_is_finished(VALUE self)
 {
   http_parser *http = NULL;
   DATA_GET(self, http_parser, http);
-  
+
   return http_parser_is_finished(http) ? Qtrue : Qfalse;
 }
 
@@ -328,32 +332,32 @@ VALUE HttpParser_nread(VALUE self)
 {
   http_parser *http = NULL;
   DATA_GET(self, http_parser, http);
-  
+
   return INT2FIX(http->nread);
 }
 
 
 void URIClassifier_free(void *data)
 {
-    TRACE();
-    
-    if(data) {
-      tst_cleanup((struct tst *)data);
-    }
+  TRACE();
+
+  if(data) {
+    tst_cleanup((struct tst *)data);
+  }
 }
 
 
 
 VALUE URIClassifier_alloc(VALUE klass)
 {
-    VALUE obj;
-    struct tst *tst = tst_init(TRIE_INCREASE);
-    TRACE();
-    assert(tst && "failed to initialize trie structure");
+  VALUE obj;
+  struct tst *tst = tst_init(TRIE_INCREASE);
+  TRACE();
+  assert(tst && "failed to initialize trie structure");
 
-    obj = Data_Wrap_Struct(klass, NULL, URIClassifier_free, tst);
+  obj = Data_Wrap_Struct(klass, NULL, URIClassifier_free, tst);
 
-    return obj;
+  return obj;
 }
 
 /**
@@ -415,7 +419,7 @@ VALUE URIClassifier_register(VALUE self, VALUE uri, VALUE handler)
   } else if(rc == TST_NULL_KEY) {
     rb_raise(rb_eStandardError, "URI was empty");
   }
-  
+
   rb_hash_aset(rb_ivar_get(self, id_handler_map), uri, handler);
 
   return Qnil;
@@ -504,7 +508,7 @@ VALUE URIClassifier_resolve(VALUE self, VALUE uri)
       // matches a script so process like normal
       rb_ary_push(result, rb_str_substr(uri, pref_len, RSTRING(uri)->len));
     }
-      
+
     rb_ary_push(result, (VALUE)handler);
   } else {
     // not found so push back nothing
@@ -521,7 +525,6 @@ void Init_http11()
 {
 
   mMongrel = rb_define_module("Mongrel");
-  id_handler_map = rb_intern("@handler_map");
 
   DEF_GLOBAL(http_prefix, "HTTP_");
   DEF_GLOBAL(request_method, "REQUEST_METHOD");
@@ -539,7 +542,7 @@ void Init_http11()
   DEF_GLOBAL(server_protocol, "SERVER_PROTOCOL");
   DEF_GLOBAL(server_protocol_value, "HTTP/1.1");
   DEF_GLOBAL(http_host, "HTTP_HOST");
-  DEF_GLOBAL(mongrel_version, "Mongrel 0.3.13.3");
+  DEF_GLOBAL(mongrel_version, "Mongrel 0.3.13.4");
   DEF_GLOBAL(server_software, "SERVER_SOFTWARE");
   DEF_GLOBAL(port_80, "80");
 
@@ -562,5 +565,5 @@ void Init_http11()
   rb_define_method(cURIClassifier, "unregister", URIClassifier_unregister, 1);
   rb_define_method(cURIClassifier, "resolve", URIClassifier_resolve, 1);
 }
-
+
 
diff --git a/lib/mongrel.rb b/lib/mongrel.rb
index f2f1a02..a801148 100644
--- a/lib/mongrel.rb
+++ b/lib/mongrel.rb
@@ -14,6 +14,7 @@ require 'mongrel/handlers'
 require 'mongrel/command'
 require 'mongrel/tcphack'
 require 'yaml'
+require 'mongrel/configurator'
 require 'time'
 require 'rubygems'
 require 'etc'
@@ -33,7 +34,7 @@ module Mongrel
 
   class URIClassifier
     attr_reader :handler_map
-  
+
     # Returns the URIs that have been registered with this classifier so far.
     # The URIs returned should not be modified as this will cause a memory leak.
     # You can use this to inspect the contents of the URIClassifier.
@@ -120,13 +121,13 @@ module Mongrel
     # The original URI requested by the client.  Passed to URIClassifier to build PATH_INFO and SCRIPT_NAME.
     REQUEST_URI='REQUEST_URI'.freeze
 
-    MONGREL_VERSION="0.3.13.3".freeze
+    MONGREL_VERSION="0.3.13.4".freeze
 
     # TODO: this use of a base for tempfiles needs to be looked at for security problems
     MONGREL_TMP_BASE="mongrel".freeze
 
     # The standard empty 404 response for bad requests.  Use Error4040Handler for custom stuff.
-    ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: #{MONGREL_VERSION}\r\n\r\nNOT FOUND".freeze
+    ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Mongrel #{MONGREL_VERSION}\r\n\r\nNOT FOUND".freeze
 
     CONTENT_LENGTH="CONTENT_LENGTH".freeze
 
@@ -163,6 +164,10 @@ module Mongrel
     REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze
   end
 
+  # Basically a Hash with one extra parameter for the HTTP body, mostly used internally.
+  class HttpParams < Hash
+    attr_accessor :http_body
+  end
 
   # When a handler is found for a registered URI then this class is constructed
   # and passed to your HttpHandler::process method.  You should assume that
@@ -186,27 +191,58 @@ module Mongrel
     # body data into the HttpRequest.body attribute.
     #
     # TODO: Implement tempfile removal when the request is done.
-    def initialize(params, initial_body, socket, notifier)
+    def initialize(params, socket, dispatcher)
       @params = params
       @socket = socket
-
-      clen = params[Const::CONTENT_LENGTH].to_i - initial_body.length
-      total = clen
-
-      if clen > Const::MAX_BODY
-        @body = Tempfile.new(Const::MONGREL_TMP_BASE)
-        @body.binmode
-      else
+      content_length = params[Const::CONTENT_LENGTH].to_i
+      http_body_len = params.http_body.length
+
+      dispatcher.request_begins(params) if dispatcher
+
+      # conditions to test:
+      #   * http_body_len == 0 && content_length == 0  -- Nothing to do
+      #   * http_body_len > content_length -- ERROR, abort
+      #   * http_body_len < content_length -- need to read more
+      #   * http_body_len == content_length -- initial body has all of it
+      if http_body_len == 0 && content_length == 0
+        # no body to process
         @body = StringIO.new
+        dispatcher.request_progress(params, 0, 0) if dispatcher
+      elsif http_body_len > content_length
+        # ERROR, they're sending bad requests
+        raise HttpParserError.new("Sent body size #{http_body_len} but declared Content-Length: #{content_length}")
+      elsif http_body_len < content_length
+        # must read more data to complete body
+        clen = content_length - http_body_len
+        if clen > Const::MAX_BODY
+          # huge body, put it in a tempfile
+          @body = Tempfile.new(Const::MONGREL_TMP_BASE)
+          @body.binmode
+        else
+          # small body, just use that
+          @body = StringIO.new(params.http_body)
+        end
+        read_body(clen, dispatcher)
+      elsif http_body_len == content_length
+        # we've got everything, pack it up
+        @body = StringIO.new(params.http_body)
+        dispatcher.request_progress(params, 0, http_body_len) if dispatcher
+      else
+        STDERR.puts "BAD LOGIC: Tell Zed he's a moron."
       end
+    end
 
-      begin
-        @body.write(initial_body)
-        notifier.request_begins(params) if notifier
 
+    # Does the heavy lifting of properly reading the larger body requests in
+    # small chunks.  It expects @body to be an IO object, @socket to be valid,
+    # and will set @body = nil if the request fails.  It also expects any initial
+    # part of the body that has been read to be in the @body already.
+    def read_body(clen, dispatcher)
+      begin
+        total = clen
         # write the odd sized chunk first
         clen -= @body.write(@socket.read(clen % Const::CHUNK_SIZE))
-        notifier.request_progress(params, clen, total) if notifier
+        dispatcher.request_progress(params, clen, total) if dispatcher
 
         # then stream out nothing but perfectly sized chunks
         while clen > 0 and !@socket.closed?
@@ -215,7 +251,7 @@ module Mongrel
           raise "Socket closed or read failure" if not data or data.length != Const::CHUNK_SIZE
           clen -= @body.write(data)
           # ASSUME: we are writing to a disk and these writes always write the requested amount
-          notifier.request_progress(params, clen, total) if notifier
+          dispatcher.request_progress(params, clen, total) if dispatcher
         end
 
         # rewind to keep the world happy
@@ -228,6 +264,7 @@ module Mongrel
       end
     end
 
+
     # Performs URI escaping so that you can construct proper
     # query strings faster.  Use this rather than the cgi.rb
     # version since it's faster.  (Stolen from Camping).
@@ -514,7 +551,7 @@ module Mongrel
     def process_client(client)
       begin
         parser = HttpParser.new
-        params = {}
+        params = HttpParams.new
         request = nil
         data = client.readpartial(Const::CHUNK_SIZE)
         nparsed = 0
@@ -535,10 +572,7 @@ module Mongrel
               params[Const::REMOTE_ADDR] = params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last
               notifier = handlers[0].request_notify ? handlers[0] : nil
 
-              # TODO: Find a faster/better way to carve out the range, preferably without copying.
-              data = data[nparsed ... data.length] || ""
-
-              request = HttpRequest.new(params, data, client, notifier)
+              request = HttpRequest.new(params, client, notifier)
 
               # in the case of large file uploads the user could close the socket, so skip those requests
               break if request.body == nil  # nil signals from HttpRequest::initialize that the request was aborted
@@ -578,6 +612,7 @@ module Mongrel
         reap_dead_workers('too many files')
       rescue Object
         STDERR.puts "#{Time.now}: ERROR: #$!"
+        STDERR.puts $!.backtrace.join("\n")
       ensure
         client.close unless client.closed?
         request.body.delete if request and request.body.class == Tempfile
@@ -646,6 +681,9 @@ module Mongrel
           rescue Errno::EMFILE
             reap_dead_workers("too many open files")
             sleep 0.5
+          rescue Errno::ECONNABORTED
+            # client closed the socket even before accept
+            client.close if not client.closed?
           end
         end
 
@@ -702,364 +740,4 @@ module Mongrel
   end
 
 
-  # 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
-
-    # Writes the PID file but only if we're on windows.
-    def write_pid_file
-      if RUBY_PLATFORM !~ /mswin/
-        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], in_front=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'
-
-        Daemonize.daemonize(log_file=File.join(ops[:cwd], ops[:log_file]))
-
-        # 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
-
-    # Let's 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 and that the pid file
-    # should be unlinked on exit.
-    def stop(needs_restart=false, unlink_pid_file=true)
-      @listeners.each {|name,s|
-        s.stop
-      }
-
-      @needs_restart = needs_restart
-      if unlink_pid_file
-        File.unlink @pid_file if (@pid_file and File.exist?(@pid_file))
-      end
-    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:  :objects, :rails, :files, :threads, :params
-    #
-    # NOTE: Use [:files] to get accesses dumped to stderr like with WEBrick.
-    def debug(location, what = [:objects, :rails, :files, :threads, :params])
-      require 'mongrel/debug'
-      handlers = {
-        :files => "/handlers/requestlog::access",
-        :rails => "/handlers/requestlog::files",
-        :objects => "/handlers/requestlog::objects",
-        :threads => "/handlers/requestlog::threads",
-        :params => "/handlers/requestlog::params"
-      }
-
-      # turn on the debugging infrastructure, and ObjectTracker is a pig
-      ObjectTracker.configure if what.include? :objects
-      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(need_restart=false) }
-
-      if RUBY_PLATFORM !~ /mswin/
-        # graceful shutdown
-        trap("TERM") { log "TERM signal received."; stop }
-
-        # restart
-        trap("USR2") { log "USR2 signal received."; stop(need_restart=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
diff --git a/lib/mongrel/configurator.rb b/lib/mongrel/configurator.rb
new file mode 100644
index 0000000..8ee6896
--- /dev/null
+++ b/lib/mongrel/configurator.rb
@@ -0,0 +1,366 @@
+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
+
+    # Writes the PID file but only if we're on windows.
+    def write_pid_file
+      if RUBY_PLATFORM !~ /mswin/
+        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], in_front=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'
+
+        Daemonize.daemonize(log_file=File.join(ops[:cwd], ops[:log_file]))
+
+        # 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
+
+    # Let's 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 and that the pid file
+    # should be unlinked on exit.
+    def stop(needs_restart=false, unlink_pid_file=true)
+      @listeners.each {|name,s|
+        s.stop
+      }
+
+      @needs_restart = needs_restart
+      if unlink_pid_file
+        File.unlink @pid_file if (@pid_file and File.exist?(@pid_file))
+      end
+    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:  :objects, :rails, :files, :threads, :params
+    #
+    # NOTE: Use [:files] to get accesses dumped to stderr like with WEBrick.
+    def debug(location, what = [:objects, :rails, :files, :threads, :params])
+      require 'mongrel/debug'
+      handlers = {
+        :files => "/handlers/requestlog::access",
+        :rails => "/handlers/requestlog::files",
+        :objects => "/handlers/requestlog::objects",
+        :threads => "/handlers/requestlog::threads",
+        :params => "/handlers/requestlog::params"
+      }
+
+      # turn on the debugging infrastructure, and ObjectTracker is a pig
+      ObjectTracker.configure if what.include? :objects
+      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(need_restart=false) }
+
+      if RUBY_PLATFORM !~ /mswin/
+        # graceful shutdown
+        trap("TERM") { log "TERM signal received."; stop }
+
+        # restart
+        trap("USR2") { log "USR2 signal received."; stop(need_restart=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
diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb
index df4397a..859013b 100644
--- a/lib/mongrel/rails.rb
+++ b/lib/mongrel/rails.rb
@@ -77,7 +77,7 @@ module Mongrel
           rescue Errno::EPIPE
             # ignored
           rescue Object => rails_error
-            STDERR.puts "Error calling Dispatcher.dispatch #{rails_error.inspect}"
+            STDERR.puts "#{Tim.now}: Error calling Dispatcher.dispatch #{rails_error.inspect}"
             STDERR.puts rails_error.backtrace.join("\n")
           ensure
             @guard.unlock unless ActionController::Base.allow_concurrency
@@ -136,6 +136,7 @@ module Mongrel
         ops[:environment] ||= "development"
         ops[:docroot] ||= "public"
         ops[:mime] ||= {}
+        ops[:prefix] ||= "/"
 
 
         $orig_dollar_quote = $".clone
@@ -148,6 +149,9 @@ module Mongrel
         if ActionController::Base.allow_concurrency
           log "[RAILS] ActionController::Base.allow_concurrency is true.  Wow, you're very brave."
         end
+
+        ActionController::AbstractRequest.relative_url_root = ops[:prefix]
+
         @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime])
       end
 
diff --git a/test/test_ws.rb b/test/test_ws.rb
index d8ad10d..5303d40 100644
--- a/test/test_ws.rb
+++ b/test/test_ws.rb
@@ -28,7 +28,9 @@ class WebServerTest < Test::Unit::TestCase
     @server = HttpServer.new("127.0.0.1", 9998,num_processors=1)
     @tester = TestHandler.new
     @server.register("/test", @tester)
-    @server.run
+    redirect_test_io do
+      @server.run
+    end
   end
 
   def teardown