about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-05-20 02:56:30 +0000
committerzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-05-20 02:56:30 +0000
commitef96cf124840fc9b0770a18ba13d6638fcdaa189 (patch)
tree839eab2bd7ceb37a27465a68b8e4b4459f4400b6
parent76f29c4aa2c010ab1e70ede2bcc585307e4e1d07 (diff)
downloadunicorn-ef96cf124840fc9b0770a18ba13d6638fcdaa189.tar.gz
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@190 19e92222-5c0b-0410-8929-a290d50e31e9
-rw-r--r--bin/mongrel_rails32
-rw-r--r--lib/mongrel.rb232
-rw-r--r--lib/mongrel/rails.rb55
-rw-r--r--test/test_configurator.rb2
-rw-r--r--tools/rakehelp.rb31
-rw-r--r--tools/trickletest.rb37
6 files changed, 241 insertions, 148 deletions
diff --git a/bin/mongrel_rails b/bin/mongrel_rails
index 58fd615..36392af 100644
--- a/bin/mongrel_rails
+++ b/bin/mongrel_rails
@@ -37,7 +37,7 @@ class Start < GemPlugin::Plugin "/commands"
       ['-G', '--generate CONFIG', "Generate a config file for -C", :@generate, nil]
     ]
   end
-  
+
   def validate
     @cwd = File.expand_path(@cwd)
     valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
@@ -74,22 +74,22 @@ class Start < GemPlugin::Plugin "/commands"
       conf = YAML.load_file(@config_file)
       settings = conf.merge(settings)
     end
-    
+
     config = Mongrel::Rails::RailsConfigurator.new(settings) do
       log "Starting Mongrel in #{defaults[:environment]} mode at #{defaults[:host]}:#{defaults[:port]}"
-      
+
       if defaults[:daemon]
         log "Daemonizing, any open files are closed.  Look at #{defaults[:pid_file]} and #{defaults[:log_file]} for info."
         daemonize
       end
-      
+
       listener do
         mime = {}
         if defaults[:mime_map]
           log "Loading additional MIME types from #{defaults[:mime_map]}"
           mime = load_mime_map(defaults[:mime_map], mime)
         end
-        
+
         if defaults[:debug]
           log "Installing debugging prefixed filters.  Look in log/mongrel_debug for the files."
           debug "/"
@@ -135,7 +135,7 @@ def send_signal(signal, pid_file)
   rescue Errno::ESRCH
     puts "Process does not exist.  Not running."
   end
-  
+
   puts "Done."
 end
 
@@ -145,12 +145,12 @@ class Stop < GemPlugin::Plugin "/commands"
 
   def configure
     options [
-             ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd],
-             ['-f', '--force', "Force the shutdown.", :@force, false],
-             ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/mongrel.pid"]
+      ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd],
+      ['-f', '--force', "Force the shutdown.", :@force, false],
+      ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/mongrel.pid"]
     ]
   end
-  
+
   def validate
     @cwd = File.expand_path(@cwd)
     valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
@@ -161,7 +161,7 @@ class Stop < GemPlugin::Plugin "/commands"
     return @valid
   end
 
-  
+
   def run
     if @force
       send_signal("KILL", @pid_file)
@@ -178,12 +178,12 @@ class Restart < GemPlugin::Plugin "/commands"
 
   def configure
     options [
-             ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd],
-             ['-s', '--soft', "Do a soft restart rather than a process exit restart", :@soft, false],
-             ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/mongrel.pid"]
+      ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd],
+      ['-s', '--soft', "Do a soft restart rather than a process exit restart", :@soft, false],
+      ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/mongrel.pid"]
     ]
   end
-  
+
   def validate
     @cwd = File.expand_path(@cwd)
     valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
@@ -209,7 +209,7 @@ GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => Gem
 
 
 if not Mongrel::Command::Registry.instance.run ARGV
-   exit 1
+  exit 1
 end
 
 
diff --git a/lib/mongrel.rb b/lib/mongrel.rb
index 6efdad4..ed72744 100644
--- a/lib/mongrel.rb
+++ b/lib/mongrel.rb
@@ -88,7 +88,7 @@ module Mongrel
     505  => 'HTTP Version not supported'
   }
 
-  
+
 
   # Frequently used constants when constructing requests or responses.  Many times
   # the constant just refers to a string with the same contents.  Using these constants
@@ -156,7 +156,7 @@ module Mongrel
   # HttpRequest.params Hash that matches common CGI params, and a HttpRequest.body
   # which is a string containing the request body (raw for now).
   #
-  # The HttpeRequest.initialize method will convert any request that is larger than
+  # The HttpRequest.initialize method will convert any request that is larger than
   # Const::MAX_BODY into a Tempfile and use that as the body.  Otherwise it uses
   # a StringIO object.  To be safe, you should assume it works like a file.  
   class HttpRequest
@@ -165,12 +165,14 @@ module Mongrel
     # You don't really call this.  It's made for you.
     # Main thing it does is hook up the params, and store any remaining
     # body data into the HttpRequest.body attribute.
+    #
+    # TODO: Implement tempfile removal when the request is done.
     def initialize(params, initial_body, socket)
       @params = params
       @socket = socket
 
       clen = params[Const::CONTENT_LENGTH].to_i - initial_body.length
-      
+
       if clen > Const::MAX_BODY
         @body = Tempfile.new(self.class.name)
         @body.binmode
@@ -183,13 +185,14 @@ module Mongrel
 
         # write the odd sized chunk first
         clen -= @body.write(@socket.read(clen % Const::CHUNK_SIZE))
-        
+
         # then stream out nothing but perfectly sized chunks
         while clen > 0
           data = @socket.read(Const::CHUNK_SIZE)
           # have to do it this way since @socket.eof? causes it to block
           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
         end
 
         # rewind to keep the world happy
@@ -262,7 +265,7 @@ module Mongrel
     def[]=(key,value)
       @out.write(Const::HEADER_FORMAT % [key, value])
     end
-    
+
   end
 
   # Writes and controls your response to the client using the HTTP/1.1 specification.
@@ -303,7 +306,7 @@ module Mongrel
     attr_reader :body_sent
     attr_reader :header_sent
     attr_reader :status_sent
-    
+
     def initialize(socket)
       @socket = socket
       @body = StringIO.new
@@ -345,7 +348,7 @@ module Mongrel
 
     def send_status(content_length=nil)
       if not @status_sent
-        content_length ||= @body.length
+        content_length ||= @body.length
         @socket.write(Const::STATUS_FORMAT % [status, HTTP_STATUS_CODES[@status], content_length])
         @status_sent = true
       end
@@ -362,7 +365,6 @@ module Mongrel
     def send_body
       if not @body_sent
         @body.rewind
-        # connection: close is also added to ensure that the client does not pipeline.
         @socket.write(@body.read)
         @body_sent = true
       end
@@ -372,6 +374,10 @@ module Mongrel
     # reading and written in chunks to the socket.  If the
     # <a href="http://rubyforge.org/projects/ruby-sendfile">sendfile</a> library is found,
     # it is used to send the file, often with greater speed and less memory/cpu usage.
+    #
+    # The presence of ruby-sendfile is determined by @socket.response_to? :sendfile, which means
+    # that if you have your own sendfile implementation you can use it without changing this function,
+    # just make sure it follows the ruby-sendfile signature.
     def send_file(path)
       File.open(path, "rb") do |f|
         if @socket.respond_to? :sendfile
@@ -384,7 +390,7 @@ module Mongrel
       end
     rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
       # ignore these since it means the client closed off early
-      STDERR.puts "Client closed socket requesting file #{req}: #$!"
+      STDERR.puts "Client closed socket early requesting file #{req}: #$!"
     end
 
     def write(data)
@@ -404,9 +410,6 @@ module Mongrel
     end
 
   end
-  
-
-
 
   # This is the main driver of Mongrel, while the Mognrel::HttpParser and Mongrel::URIClassifier
   # make up the majority of how the server functions.  It's a very simple class that just
@@ -446,6 +449,8 @@ module Mongrel
     # The timeout parameter is a sleep timeout (in hundredths of a second) that is placed between
     # socket.accept calls in order to give the server a cheap throttle time.  It defaults to 0 and
     # actually if it is 0 then the sleep is not done at all.
+    #
+    # TODO: Find out if anyone actually uses the timeout option since it seems to cause problems on FBSD.
     def initialize(host, port, num_processors=(2**30-1), timeout=0)
       @socket = TCPServer.new(host, port)
       @classifier = URIClassifier.new
@@ -456,7 +461,7 @@ module Mongrel
       @num_processors = num_processors
       @death_time = 60
     end
-    
+
 
     # Does the majority of the IO processing.  It has been written in Ruby using
     # about 7 different IO processing strategies and no matter how it's done
@@ -467,16 +472,17 @@ module Mongrel
       begin
         parser = HttpParser.new
         params = {}
-        
+
         data = client.readpartial(Const::CHUNK_SIZE)
         nparsed = 0
-        
+
         # Assumption: nparsed will always be less since data will get filled with more
         # after each parsing.  If it doesn't get more then there was a problem
-        # with the read operation on the client socket.
+        # with the read operation on the client socket.  Effect is to stop processing when the
+        # socket can't fill the buffer for further parsing.
         while nparsed < data.length
           nparsed = parser.execute(params, data, nparsed)
-          
+
           if parser.finished?
             script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_URI])
 
@@ -484,29 +490,35 @@ module Mongrel
               params[Const::PATH_INFO] = path_info
               params[Const::SCRIPT_NAME] = script_name
               params[Const::REMOTE_ADDR] = params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last
-              
+
+              # TODO: Find a faster/better way to carve out the range, preferrably without copying.
               request = HttpRequest.new(params, data[nparsed ... data.length] || "", client)
-              
+
               # in the case of large file uploads the user could close the socket, so skip those requests
-              break if request.body == nil
+              break if request.body == nil  # nil signals from HttpRequest::initialize that the request was aborted
 
               # request is good so far, continue processing the response
               response = HttpResponse.new(client)
-              
+
+              # Process each handler in registered order until we run out or one finalizes the response.
               handlers.each do |handler|
                 handler.process(request, response)
                 break if response.done
               end
-              
+
+              # And finally, if nobody closed the response off, we finalize it.
               if not response.done
                 response.finished
               end
             else
+              # Didn't find it, return a stock 404 response.
+              # TODO: Implement customer 404 files (but really they should use a real web server).
               client.write(Const::ERROR_404_RESPONSE)
             end
-          
+
             break #done
           else
+            # Parser is not done, queue up more data to read and continue parsing
             data << client.readpartial(Const::CHUNK_SIZE)
             if data.length >= Const::MAX_HEADER
               raise HttpParserError.new("HEADER is longer than allowed, aborting client early.")
@@ -539,7 +551,7 @@ module Mongrel
         end
       end
     end
-      
+
 
     # Runs the thing.  It returns the thread used so you can "join" it.  You can also
     # access the HttpServer::acceptor attribute to get the thread later.
@@ -560,11 +572,11 @@ module Mongrel
               thread = Thread.new do
                 process_client(client)
               end
-              
+
               thread[:started_on] = Time.now
               thread.priority=1
               @workers.add(thread)
-              
+
               sleep @timeout/100 if @timeout > 0
             end
           rescue StopServer
@@ -578,9 +590,10 @@ module Mongrel
         end
 
         # troll through the threads that are waiting and kill any that take too long
+        # TODO: Allow for death time to be set if people ask for it.
         @death_time = 10
         shutdown_start = Time.now
-        
+
         while @workers.list.length > 0
           waited_for = (Time.now - shutdown_start).ceil
           STDERR.print "Shutdown waited #{waited_for} for #{@workers.list.length} requests, could take #{@death_time + @timeout} seconds.\r" if @workers.list.length > 0
@@ -592,12 +605,13 @@ module Mongrel
       return @acceptor
     end
 
-    
+
     # Simply registers a handler with the internal URIClassifier.  When the URI is
     # found in the prefix of a request then your handler's HttpHandler::process method
     # is called.  See Mongrel::URIClassifier#register for more information.
     #
     # If you set in_front=true then the passed in handler will be put in front in the list.
+    # Otherwise it's placed at the end of the list.
     def register(uri, handler, in_front=false)
       script_name, path_info, handlers = @classifier.resolve(uri)
 
@@ -671,31 +685,40 @@ module Mongrel
 
     # 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
-      
+
       if blk
         cloaker(&blk).bind(self).call
       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
-      (class << self; self; end).class_eval do
+    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
@@ -706,9 +729,10 @@ module Mongrel
     # * :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 not timeout)
+    # * :timeout => 1/100th of a second timeout between requests. (10 is 1/10th, 0 is timeout)
     #
     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
@@ -716,30 +740,31 @@ module Mongrel
       @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
-      
+
+      # 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
+
+      # 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 => Handler to use for this location.
-    # * :in_front => Rather than appending, it prepends this handler.
+    # * :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
@@ -753,33 +778,33 @@ module Mongrel
     # * :log_file => Where to write STDOUT and STDERR.
     # * :pid_file => Where to write the process ID.
     #
-    # It is safe to call this on win32 as it will only require daemons
-    # if NOT win32.
+    # 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])
-        
+
         open(ops[:pid_file],"w") {|f| f.write(Process.pid) }
       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 loading.
+    # 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|
@@ -795,14 +820,14 @@ module Mongrel
 
       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.
@@ -812,14 +837,14 @@ module Mongrel
     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.
@@ -827,8 +852,8 @@ module Mongrel
       ops = resolve_defaults(options)
       GemPlugin::Manager.instance.create(name, ops)
     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.
@@ -837,16 +862,23 @@ module Mongrel
         log "Running #{name} listener."
         s.run
       }
-      
+
     end
-    
+
     # Calls .stop on all the configured listeners so they
-    # stop processing requests (gracefully).
-    def stop
+    # 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|
         log "Stopping #{name} listener."
         s.stop
       }
+
+      @needs_restart = needs_restart
+      if unlink_pid_file
+        File.unlink @pid_file if (@pid_file and File.exist?(@pid_file))
+      end
     end
 
 
@@ -863,25 +895,32 @@ module Mongrel
     # 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.
-    def debug(location)
+    #
+    # TODO: Document the optional selections from the what parameter
+    def debug(location, what = [:object, :rails, :files, :threads, :params])
       require 'mongrel/debug'
-      ObjectTracker.configure
+      handlers = {
+        :object => "/handlers/requestlog::access",
+        :rails => "/handlers/requestlog::files",
+        :files => "/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? :object
       MongrelDbg.configure
-      MongrelDbg.begin_trace :objects
-      MongrelDbg.begin_trace :rails
-      MongrelDbg.begin_trace :files
-      MongrelDbg.begin_trace :threads
-      
-      uri location, :handler => plugin("/handlers/requestlog::access")
-      uri location, :handler => plugin("/handlers/requestlog::files")
-      uri location, :handler => plugin("/handlers/requestlog::objects")
-      uri location, :handler => plugin("/handlers/requestlog::params")
-      uri location, :handler => plugin("/handlers/requestlog::threads")
+
+      # 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
-    # reads it in and does an eval on the contents passing in the right
+    # 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}) }
@@ -899,30 +938,19 @@ module Mongrel
     # This command is safely ignored if the platform is win32 (with a warning)
     def setup_signals(options={})
       ops = resolve_defaults(options)
-      
+
+      @pid_file = ops[:pid_file]
+
       if RUBY_PLATFORM !~ /mswin/
         # graceful shutdown
-        trap("TERM") {
-          log "TERM signal received."
-          stop
-          File.unlink ops[:pid_file] if File.exist?(ops[:pid_file])
-        }
-        
+        trap("TERM") { log "TERM signal received."; stop }
+
         # restart
-        trap("USR2") {
-          log "USR2 signal received."
-          stop
-          File.unlink ops[:pid_file] if File.exist?(ops[:pid_file])
-          @needs_restart = true
-        }
-        
-        trap("INT") {
-          log "INT signal received."
-          stop
-          File.unlink ops[:pid_file] if File.exist?(ops[:pid_file])
-          @needs_restart = false
-        }
-        
+        trap("USR2") { log "USR2 signal received."; stop(need_restart=true) }
+
+        # forced shutdown, even if previously restarted (actually just like TERM but for CTRL-C)
+        trap("INT") { log "INT signal received."; stop(need_restart=false) }
+
         log "Signals ready.  TERM => stop.  USR2 => restart.  INT => stop (no restart)."
       else
         log "WARNING: Win32 does not have signals support."
@@ -933,7 +961,7 @@ module Mongrel
     def log(msg)
       STDERR.print "** ", msg, "\n"
     end
-    
+
   end
 
 end
diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb
index 611fcd0..207ae4b 100644
--- a/lib/mongrel/rails.rb
+++ b/lib/mongrel/rails.rb
@@ -28,15 +28,15 @@ module Mongrel
     class RailsHandler < Mongrel::HttpHandler
       attr_reader :files
       attr_reader :guard
-      
+
       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:
       #
       #
@@ -45,12 +45,12 @@ module Mongrel
       # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go.
       def process(request, response)
         return if response.socket.closed?
-        
+
         path_info = request.params[Mongrel::Const::PATH_INFO]
         page_cached = request.params[Mongrel::Const::PATH_INFO] + ".html"
-        
+
         if @files.can_serve(path_info)
-            # File exists as-is so serve it up
+          # File exists as-is so serve it up
           @files.process(request,response)
         elsif @files.can_serve(page_cached)
           # possible cached page, serve it up      
@@ -60,12 +60,12 @@ module Mongrel
           begin
             cgi = Mongrel::CGIWrapper.new(request, response)
             cgi.handler = self
-            
+
             @guard.synchronize do
               # Rails is not thread safe so must be run entirely within synchronize
               Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body)
             end
-            
+
             # This finalizes the output using the proper HttpResponse way
             cgi.out {""}
           rescue Errno::EPIPE
@@ -76,8 +76,8 @@ module Mongrel
           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!
@@ -93,7 +93,7 @@ module Mongrel
     # 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
@@ -119,28 +119,28 @@ module Mongrel
       # you want, but 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'
-        
+
         @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime])
       end
-      
-      
+
+
       # Reloads rails.  This isn't too reliable really, but
       # should work for most minimal reload purposes.  Only reliable
       # way it so stop then start the process.
@@ -148,27 +148,24 @@ module Mongrel
         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)
-        
+
         if RUBY_PLATFORM !~ /mswin/
           setup_signals(options)
-          
+
           # rails reload
-          trap("HUP") {
-            log "HUP signal received."
-            reload!
-          }
-          
+          trap("HUP") { log "HUP signal received."; reload!          }
+
           log "Rails signals registered.  HUP => reload (without restart).  It might not work well."
         else
           log "WARNING:  Rails does not support signals on Win32."
diff --git a/test/test_configurator.rb b/test/test_configurator.rb
index e9dea36..e440c6a 100644
--- a/test/test_configurator.rb
+++ b/test/test_configurator.rb
@@ -58,7 +58,7 @@ class ConfiguratorTest < Test::Unit::TestCase
     assert $test_plugin_fired == 6, "Test filter plugin didn't run 6 times."
 
     config.stop
-    
+
     assert_raise Errno::EBADF, Errno::ECONNREFUSED do
       res = Net::HTTP.get(URI.parse("http://localhost:4501/"))
     end
diff --git a/tools/rakehelp.rb b/tools/rakehelp.rb
index 3cda4a1..a630d1b 100644
--- a/tools/rakehelp.rb
+++ b/tools/rakehelp.rb
@@ -109,3 +109,34 @@ def sub_project(project, *targets)
     sh %{cd projects/#{project}; rake #{target.to_s}; }
   end
 end
+
+#==============================================================
+# A set of rake tasks for using the Rcov code coverage utility
+# http://www.soapboxsoftware.org/articles/2006/05/06/another-rake-task-for-rcov
+#==============================================================
+
+require 'rake/clean'
+
+rcov_path = ENV['RCOV_PATH'] ? ENV['RCOV_PATH'] : "#{Config::CONFIG['bindir']}/rcov"
+rcov_test_output = "./test/coverage"
+rcov_unit_test_output = "#{rcov_test_output}/unit"
+rcov_exclude="rubygems,test/test_"
+# Add our created paths to the 'rake clobber' list
+CLOBBER.include(rcov_unit_test_output)
+
+desc 'Removes all previous unit test coverage information'
+task (:reset_unit_test_coverage) do |t|
+  mkdir_p rcov_unit_test_output
+  rm_rf rcov_unit_test_output
+  mkdir rcov_unit_test_output
+end
+
+desc 'Run all unit tests with Rcov to measure coverage'
+Rake::TestTask.new(:test_units_with_coverage => [ :reset_unit_test_coverage ]) do |t|
+  t.libs << "test"
+  t.pattern = 'test/**/*test*.rb'
+  t.ruby_opts << rcov_path
+  t.ruby_opts << "-o #{rcov_unit_test_output} -x #{rcov_exclude}"
+  t.verbose = true
+end
+
diff --git a/tools/trickletest.rb b/tools/trickletest.rb
new file mode 100644
index 0000000..cf3999b
--- /dev/null
+++ b/tools/trickletest.rb
@@ -0,0 +1,37 @@
+require 'socket'
+require 'stringio'
+
+def do_test(st, chunk)
+  s = TCPSocket.new('127.0.0.1',ARGV[0].to_i);
+  req = StringIO.new(st)
+  
+  while data = req.read(chunk)
+    puts "write #{data.length}: '#{data}'"
+    s.write(data)
+    s.flush
+    sleep 0.1
+  end
+  s.close
+end
+
+
+st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n"
+
+threads = []
+ARGV[1].to_i.times do
+  threads << Thread.new do
+    (st.length - 1).times do |chunk|
+      puts ">>>> #{chunk+1} sized chunks"
+      do_test(st, chunk+1)
+    end
+
+    1000.times do
+      do_test(st, rand(st.length) + 1)
+    end
+    
+  end
+
+  sleep(1+rand)
+end
+
+threads.each {|t| t.join}