diff options
author | zedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9> | 2006-02-28 07:04:41 +0000 |
---|---|---|
committer | zedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9> | 2006-02-28 07:04:41 +0000 |
commit | d1a01c03f71c14e5d4fe66d93c5ab444c0aba554 (patch) | |
tree | d7d4df48f1e708dfceea7bd78907177e5b1be889 | |
parent | 4e5132f63a210beb766ebfe52bea7424903403ae (diff) | |
download | unicorn-d1a01c03f71c14e5d4fe66d93c5ab444c0aba554.tar.gz |
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@65 19e92222-5c0b-0410-8929-a290d50e31e9
-rw-r--r-- | bin/mongrel_rails | 51 | ||||
-rw-r--r-- | bin/mongrel_rails_service | 14 | ||||
-rw-r--r-- | bin/mongrel_rails_svc | 102 | ||||
-rw-r--r-- | lib/mongrel.rb | 18 | ||||
-rw-r--r-- | lib/mongrel/command.rb | 38 | ||||
-rw-r--r-- | lib/mongrel/plugins.rb | 78 | ||||
-rw-r--r-- | lib/mongrel/rails.rb | 68 | ||||
-rw-r--r-- | lib/pluginfactory.rb | 384 | ||||
-rw-r--r-- | test/plugins/commands/test1.rb | 8 | ||||
-rw-r--r-- | test/test_plugins.rb | 11 |
10 files changed, 224 insertions, 548 deletions
diff --git a/bin/mongrel_rails b/bin/mongrel_rails index 9d58628..c9051fe 100644 --- a/bin/mongrel_rails +++ b/bin/mongrel_rails @@ -1,47 +1,9 @@ require 'rubygems' -require 'mongrel' -require 'mongrel/command' +require 'mongrel/rails' -class RailsHandler < Mongrel::HttpHandler - - 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 - - def process(request, response) - # not static, need to talk to rails - return if response.socket.closed? - - if @files.can_serve(request.params["PATH_INFO"]) - @files.process(request,response) - else - cgi = Mongrel::CGIWrapper.new(request, response) - - begin - @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 Object => rails_error - STDERR.puts "calling Dispatcher.dispatch #{rails_error}" - STDERR.puts rails_error.backtrace.join("\n") - end - end - - end -end - - - -class StartCommand < Mongrel::Command::Command +class Start < Mongrel::Plugin "/commands" + include Mongrel::Command::Command def configure options [ @@ -177,7 +139,9 @@ def send_signal(signal, pid_file) puts "Done." end -class StopCommand < Mongrel::Command::Command + +class Stop < Mongrel::Plugin "/commands" + include Mongrel::Command::Command def configure options [ @@ -211,7 +175,8 @@ end -class RestartCommand < Mongrel::Command::Command +class Restart < Mongrel::Plugin "/commands" + include Mongrel::Command::Command def configure options [ diff --git a/bin/mongrel_rails_service b/bin/mongrel_rails_service index 50131ae..352219c 100644 --- a/bin/mongrel_rails_service +++ b/bin/mongrel_rails_service @@ -7,8 +7,6 @@ ###############################################
require 'rubygems'
require 'mongrel'
-require 'mongrel/command'
-
require 'win32/service'
include Win32
@@ -32,7 +30,8 @@ module GenericCommand end
end
-class InstallCommand < Mongrel::Command::Command
+class InstallCommand < Mongrel::Plugin "/commands"
+ include Mongrel::Command::Command
# Default every option to nil so only the defined ones get passed to service
# (which will override ServiceCommand defaults).
@@ -175,7 +174,8 @@ class InstallCommand < Mongrel::Command::Command end
end
-class DeleteCommand < Mongrel::Command::Command
+class Delete < Mongrel::Plugin "/commands"
+ include Mongrel::Command::Command
include GenericCommand
def run
@@ -193,7 +193,8 @@ class DeleteCommand < Mongrel::Command::Command end
end
-class StartCommand < Mongrel::Command::Command
+class Start < Mongrel::Plugin "/commands"
+ include Mongrel::Command::Command
include GenericCommand
def run
@@ -217,7 +218,8 @@ class StartCommand < Mongrel::Command::Command end
end
-class StopCommand < Mongrel::Command::Command
+class Stop < Mongrel::Plugin "/commands"
+ include Mongrel::Command::Command
include GenericCommand
def run
diff --git a/bin/mongrel_rails_svc b/bin/mongrel_rails_svc index bc65d66..5e19e82 100644 --- a/bin/mongrel_rails_svc +++ b/bin/mongrel_rails_svc @@ -4,10 +4,8 @@ # This is where Win32::Daemon resides.
###############################################
require 'rubygems'
-require 'mongrel'
-
+require 'mongrel/rails'
require 'optparse'
-
require 'win32/service'
# We need to use OpenProcess and SetProcessAffinityMask on WinNT/2K/XP for
@@ -43,39 +41,12 @@ end DEBUG_LOG_FILE = File.expand_path(File.dirname(__FILE__) + '/debug.log')
DEBUG_THREAD_LOG_FILE = File.expand_path(File.dirname(__FILE__) + '/debug_thread.log')
-class RailsHandler < Mongrel::HttpHandler
- 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
+def dbg(msg)
+ File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - #{msg}") }
+end
- def process(request, response)
- # not static, need to talk to rails
- return if response.socket.closed?
-
- if @files.can_serve(request.params["PATH_INFO"])
- @files.process(request,response)
- else
- cgi = Mongrel::CGIWrapper.new(request, response)
-
- begin
- @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 Object => rails_error
- STDERR.puts "calling Dispatcher.dispatch #{rails_error}"
- STDERR.puts rails_error.backtrace.join("\n")
- end
- end
- end
-
+def dbg_th(msg)
+ File.open(DEBUG_THREAD_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - #{msg}") }
end
# This class encapsulate the handler registering, http_server and working thread
@@ -83,7 +54,7 @@ end # (in case you don't want use mongrel_rails script)
class MongrelRails
def initialize(ip, port, rails_root, docroot, environment, mime_map, num_procs, timeout)
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - mongrelrails_initialize entered") }
+ dbg "mongrelrails_initialize entered"
@ip = ip
@port = port
@@ -94,24 +65,23 @@ class MongrelRails @num_procs = num_procs
@timeout = timeout
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - mongrelrails_initialize left") }
+ dbg "mongrelrails_initialize left"
end
def delayed_initialize
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - delayed_initialize entered") }
+ dbg "delayed_initialize entered"
@rails = configure_rails
- #@rails = SimpleHandler.new
# start up mongrel with the right configurations
@server = Mongrel::HttpServer.new(@ip, @port, @num_procs.to_i, @timeout.to_i)
@server.register("/", @rails)
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - delayed_initialize left") }
+ dbg "delayed_initialize left"
end
def load_mime_map
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - load_mime_map entered") }
+ dbg "load_mime_map entered"
mime = {}
@@ -124,13 +94,13 @@ class MongrelRails mime.each {|k,v| puts "WARNING: MIME type #{k} must start with '.'" if k.index(".") != 0 }
end
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - load_mime_map left") }
+ dbg "load_mime_map left"
return mime
end
def configure_rails
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - configure_rails entered") }
+ dbg "configure_rails entered"
Dir.chdir(@rails_root)
@@ -140,85 +110,85 @@ class MongrelRails # configure the rails handler
rails = RailsHandler.new(@docroot, load_mime_map)
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - configure_rails left") }
+ dbg "configure_rails left"
return rails
end
def start_serve
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - start_serve entered") }
+ dbg "start_serve entered"
@runner = Thread.new do
- File.open(DEBUG_THREAD_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - runner_thread suspended") }
+ dbg_th "runner_thread suspended"
Thread.stop
- File.open(DEBUG_THREAD_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - runner_thread resumed") }
- File.open(DEBUG_THREAD_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - runner_thread acceptor.join") }
+ dbg_th "runner_thread resumed"
+ dbg_th "runner_thread acceptor.join"
@server.acceptor.join
end
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - server.run") }
+ dbg "server.run"
@server.run
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - runner.run") }
+ dbg "runner.run"
@runner.run
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - start_serve left") }
+ dbg "start_serve left"
end
def stop_serve
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - stop_serve entered") }
+ dbg "stop_serve entered"
if @runner.alive?
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - killing thread") }
+ dbg "killing thread"
@runner.kill
end
@server.stop
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - stop_serve left") }
+ dbg "stop_serve left"
end
end
class RailsDaemon < Win32::Daemon
def initialize(rails)
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - daemon_initialize entered") }
+ dbg "daemon_initialize entered"
@rails = rails
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - daemon_initialize left") }
+ dbg "daemon_initialize left"
end
def service_init
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - service_init entered") }
+ dbg "service_init entered"
@rails.delayed_initialize
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - service_init left") }
+ dbg "service_init left"
end
def service_main
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - service_main entered") }
+ dbg "service_main entered"
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - rails.start_serve") }
+ dbg "rails.start_serve"
@rails.start_serve
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - while RUNNING") }
+ dbg "while RUNNING"
while state == RUNNING
sleep 1
end
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - state !RUNNING") }
+ dbg "state !RUNNING"
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - rails.stop_serve") }
+ dbg "rails.stop_serve"
@rails.stop_serve
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - service_main left") }
+ dbg "service_main left"
end
def service_stop
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - service_stop entered") }
+ dbg "service_stop entered"
- File.open(DEBUG_LOG_FILE,"a+") { |f| f.puts("#{Time.now} - service_stop left") }
+ dbg "service_stop left"
end
end
diff --git a/lib/mongrel.rb b/lib/mongrel.rb index dc928bc..8fccc90 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -4,8 +4,9 @@ require 'thread' require 'stringio' require 'mongrel/cgi' require 'mongrel/handlers' +require 'mongrel/command' require 'mongrel/plugins' - +require 'timeout' # Mongrel module containing all of the classes (include C extensions) for running # a Mongrel web server. It contains a minimalist HTTP server with just enough @@ -31,6 +32,10 @@ module Mongrel class StopServer < Exception end + # Used to timeout worker threads that have taken too long + class TimeoutWorker < Exception + end + # Every standard HTTP code mapped to the appropriate message. These are # used so frequently that they are placed directly in Mongrel for easy # access rather than Mongrel::Const. @@ -346,15 +351,18 @@ module Mongrel @host = host @port = port @processors = [] - @timeout = timeout - num_processors.times {|i| + # create the worker threads + num_processors.times do |i| @processors << Thread.new do while client = @req_queue.deq - process_client(client) + Timeout::timeout(timeout) do + process_client(client) + end end end - } + end + end diff --git a/lib/mongrel/command.rb b/lib/mongrel/command.rb index 3625438..04af386 100644 --- a/lib/mongrel/command.rb +++ b/lib/mongrel/command.rb @@ -1,7 +1,6 @@ require 'singleton' require 'optparse' -require 'pluginfactory' - +require 'mongrel/plugins' module Mongrel @@ -10,17 +9,10 @@ module Mongrel module Command - # A Command pattern implementation used to create the set of command available to the user # from Mongrel. The script uses objects which implement this interface to do the # user's bidding. - # - # Creating a new command is very easy, and you can do it without modifying the source - # of Mongrel thanks to PluginFactory. What you do is the following: - # - # 1. - class Command - include PluginFactory + module Command attr_reader :valid, :done_validating @@ -65,12 +57,6 @@ module Mongrel @opt.parse! argv end - # Tells the PluginFactory where to look for additional commands. By default - # it's just a "plugins" directory wherever we are located. - def self.derivativeDirs - return ["plugins"] - end - # Returns true/false depending on whether the command is configured properly. def validate return @valid @@ -119,8 +105,6 @@ module Mongrel end end - - # A Singleton class that manages all of the available commands # and handles running them. class Registry @@ -128,15 +112,9 @@ module Mongrel # Builds a list of possible commands from the Command derivates list def commands - list = Command.derivatives() - match = Regexp.new("(.*::.*)|(.*command.*)", Regexp::IGNORECASE) - - results = [] - list.keys.each do |key| - results << key.to_s unless match.match(key.to_s) - end - - return results.sort + pmgr = PluginManager.instance + list = pmgr.available["/commands"] + return list.sort end # Prints a list of available commands. @@ -144,7 +122,7 @@ module Mongrel puts "Available commands are:\n\n" self.commands.each do |name| - puts " - #{name}\n" + puts " - #{name[1 .. -1]}\n" end puts "\nEach command takes -h as an option to get help." @@ -165,8 +143,8 @@ module Mongrel # command exists, set it up and validate it begin - command = Command.create(cmd_name, args) - rescue FactoryError + command = PluginManager.instance.create("/commands/#{cmd_name}", args) + rescue STDERR.puts "INVALID COMMAND: #$!" print_command_list return diff --git a/lib/mongrel/plugins.rb b/lib/mongrel/plugins.rb index 9778e9b..f312e36 100644 --- a/lib/mongrel/plugins.rb +++ b/lib/mongrel/plugins.rb @@ -1,6 +1,47 @@ require 'singleton' module Mongrel + + # Implements the main method of managing plugins for Mongrel. + # "Plugins" in this sense are any classes which get registered + # with Mongrel for possible use when it's operating. These can + # be Handlers, Commands, or other classes. When you create a + # Plugin you register it into a URI-like namespace that makes + # it easy for you (and others) to reference it later during + # configuration. + # + # PluginManager is used as nothing more than a holder of all the + # plugins that have registered themselves. Let's say you have: + # + # class StopNow < Plugin "/commands" + # ... + # end + # + # Then you can get at this plugin with: + # + # cmd = PluginManager.create("/commands/stopnow") + # + # The funky syntax for StopNow is a weird trick borrowed from + # the Camping framework. See the Mongrel::Plugin *function* (yes, + # function). What this basically does is register it + # into the namespace for plugins at /commands. You could go + # as arbitrarily nested as you like. + # + # Why this strange almost second namespace? Why not just use + # the ObjectSpace and/or Modules? The main reason is speed and + # to avoid cluttering the Ruby namespace with what is really a + # configuration statement. This lets implementors put code + # into the Ruby structuring they need, and still have Plugins + # available to Mongrel via simple URI-like names. + # + # The alternative (as pluginfactory does it) is to troll through + # ObjectSpace looking for stuff that *might* be plugins every time + # one is needed. This alternative also means that you are stuck + # naming your commands in specific ways and putting them in specific + # modules in order to configure how Mongrel should use them. + # + # One downside to this is that you need to subclass plugin to + # make it work. In this case use mixins to add other functionality. class PluginManager include Singleton @@ -8,6 +49,9 @@ module Mongrel @plugins = URIClassifier.new end + # Tell the PluginManager to scan the given path (recursively) + # and load the *.rb files found there. This is how you'd + # setup your own plugin directory. def load(path) Dir.chdir(path) do Dir["**/*.rb"].each do |rbfile| @@ -16,6 +60,9 @@ module Mongrel end end + # Not necessary for you to call directly, but this is + # how Mongrel::PluginBase.inherited actually adds a + # plugin to a category. def register(category, name, klass) cat, ignored, map = @plugins.resolve(category) if not cat @@ -26,17 +73,21 @@ module Mongrel end end - + # Resolves the given name (should include /category/name) to + # find the plugin class and create an instance. It uses + # the same URIClassifier that the rest of Mongrel does so it + # is fast. def create(name, options = {}) category, plugin, map = @plugins.resolve(name) if category and plugin and plugin.length > 0 - STDERR.puts "found: #{category} #{plugin} for #{name}" map[plugin].new(options) else raise "Plugin #{name} does not exist" end end + # Returns a map of URIs->[handlers] that you can + # use to investigate available handlers. def available map = {} @plugins.uris.each do |u| @@ -50,19 +101,36 @@ module Mongrel end + # This base class for plugins reallys does nothing + # more than wire up the new class into the right category. + # It is not thread-safe yet but will be soon. class PluginBase + # See Mongrel::Plugin for an explanation. def PluginBase.inherited(klass) - - PluginManager.instance.register(@@category, klass.to_s.downcase, klass) + name = "/" + klass.to_s.downcase + PluginManager.instance.register(@@category, name, klass) end + # See Mongrel::Plugin for an explanation. def PluginBase.category=(category) @@category = category end end - def Plugin(c) + # This nifty function works with the PluginBase to give you + # the syntax: + # + # class MyThing < Plugin "/things" + # ... + # end + # + # What it does is temporarily sets the PluginBase.category, and then + # returns PluginBase. Since the next immediate thing Ruby does is + # use this returned class to create the new class, PluginBase.inherited + # gets called. PluginBase.inherited then uses the set category, class name, + # and class to register the plugin in the right way. + def Mongrel::Plugin(c) PluginBase.category = c PluginBase end diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb new file mode 100644 index 0000000..73d4272 --- /dev/null +++ b/lib/mongrel/rails.rb @@ -0,0 +1,68 @@ +require 'mongrel' + + +# 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.dispath 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 lighttpd). +class RailsHandler < Mongrel::HttpHandler + 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.dispath to have Rails go. + def process(request, response) + return if response.socket.closed? + + path_info = request.params["PATH_INFO"] + page_cached = request.params["PATH_INFO"] + ".html" + + if @files.can_serve(path_info) + # File exists as-is so serve it up + @files.process(request,response) + elsif @files.can_serve(page_cached) + # possible cached page, serve it up + request.params["PATH_INFO"] = page_cached + @files.process(request,response) + else + cgi = Mongrel::CGIWrapper.new(request, response) + + begin + @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 Object => rails_error + STDERR.puts "calling Dispatcher.dispatch #{rails_error}" + STDERR.puts rails_error.backtrace.join("\n") + end + end + end + +end diff --git a/lib/pluginfactory.rb b/lib/pluginfactory.rb deleted file mode 100644 index 0a9a32f..0000000 --- a/lib/pluginfactory.rb +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env ruby -w -# -# This module contains the PluginFactory mixin. Including PluginFactory in your -# class turns it into a factory for its derivatives, capable of searching for -# and loading them by name. This is useful when you have an abstract base class -# which defines an interface and basic functionality for a part of a larger -# system, and a collection of subclasses which implement the interface for -# different underlying functionality. -# -# An example of where this might be useful is in a program which talks to a -# database. To avoid coupling it to a specific database, you use a Driver class -# which encapsulates your program's interaction with the database behind a -# useful interface. Now you can create a concrete implementation of the Driver -# class for each kind of database you wish to talk to. If you make the base -# Driver class a PluginFactory, too, you can add new drivers simply by dropping -# them in a directory and using the Driver's <tt>create</tt> method to -# instantiate them: -# -# == Creation Argument Variants -# -# The +create+ class method added to your class by PluginFactory searches for your module using -# -# == Synopsis -# -#---##### in driver.rb #####--- -# -# require "PluginFactory" -# -# class Driver -# include PluginFactory -# def self::derivativeDirs -# ["drivers"] -# end -# end -# -#---########## -# -#---##### in drivers/mysql.rb ##### -# -# require 'driver' -# -# class MysqlDriver < Driver -# ...implementation... -# end -# -#---########## -# -#---##### in /usr/lib/ruby/1.8/PostgresDriver.rb ##### -# -# require 'driver' -# -# class PostgresDriver < Driver -# ...implementation... -# end -# -#---########## -# -#---##### elsewhere ##### -# -# require 'driver' -# -# config[:driver_type] #=> "mysql" -# driver = Driver::create( config[:driver_type] ) -# driver.class #=> MysqlDriver -# pgdriver = Driver::create( "PostGresDriver" ) -# -#---########## -# -# == Rcsid -# -# $Id: pluginfactory.rb 32 2005-03-05 00:21:05Z ged $ -# -# == Authors -# -# * Martin Chase <stillflame@FaerieMUD.org> -# * Michael Granger <ged@FaerieMUD.org> -# -# modified temporarily by Zed A. Shaw until license attribution can be given. -#--- -# -# Please see the file docs/COPYRIGHT for licensing details. -# - - -### An exception class for PluginFactory specific errors. -class FactoryError < RuntimeError - def initialize( *args ) - if ! args.empty? - msg = args.collect {|a| a.to_s}.join - super( msg ) - else - super( message ) - end - end -end # class FactoryError - - -### A mixin that adds PluginFactory class methods to a base class, so that -### subclasses may be instantiated by name. -module PluginFactory - - ### A callback for logging the various debug and information this module - ### has to log. Should take two arguments, the log level, possibly as a - ### symbol, and the log message itself. - @logger_callback = nil - class << self - attr_accessor :logger_callback - end - - ### If the logger callback is set, use it to pass on a log entry. First argument is - def self::log(level, *msg) - @logger_callback.call(level, msg.join) if @logger_callback - end - - - ### Inclusion callback -- extends the including class. - def self::included( klass ) - klass.extend( self ) - end - - - ### Raise an exception if the object being extended is anything but a - ### class. - def self::extend_object( obj ) - unless obj.is_a?( Class ) - raise TypeError, "Cannot extend a #{obj.class.name}", caller(1) - end - obj.instance_variable_set( :@derivatives, {} ) - super - end - - - ############################################################# - ### M I X I N M E T H O D S - ############################################################# - - ### Return the Hash of derivative classes, keyed by various versions of - ### the class name. - def derivatives - ancestors.each {|klass| - if klass.instance_variables.include?( "@derivatives" ) - break klass.instance_variable_get( :@derivatives ) - end - } - end - - - ### Returns the type name used when searching for a derivative. - def factoryType - base = nil - self.ancestors.each {|klass| - if klass.instance_variables.include?( "@derivatives" ) - base = klass - break - end - } - - raise FactoryError, "Couldn't find factory base for #{self.name}" if - base.nil? - - if base.name =~ /^.*::(.*)/ - return $1 - else - return base.name - end - end - - - ### Inheritance callback -- Register subclasses in the derivatives hash - ### so that ::create knows about them. - def inherited( subclass ) - keys = [ subclass.name, subclass.name.downcase, subclass ] - - # Handle class names like 'FooBar' for 'Bar' factories. - if subclass.name.match( /(?:.*::)?(\w+)(?:#{self.factoryType})/i ) - keys << Regexp.last_match[1].downcase - else - keys << subclass.name.sub( /.*::/, '' ).downcase - end - - keys.uniq.each {|key| - #PluginFactory::log :info, "Registering %s derivative of %s as %p" % - # [ subclass.name, self.name, key ] - self.derivatives[ key ] = subclass - } - super - end - - - ### Returns an Array of registered derivatives - def derivativeClasses - self.derivatives.values.uniq - end - - - ### Given the <tt>className</tt> of the class to instantiate, and other - ### arguments bound for the constructor of the new object, this method - ### loads the derivative class if it is not loaded already (raising a - ### LoadError if an appropriately-named file cannot be found), and - ### instantiates it with the given <tt>args</tt>. The <tt>className</tt> - ### may be the the fully qualified name of the class, the class object - ### itself, or the unique part of the class name. The following examples - ### would all try to load and instantiate a class called "FooListener" - ### if Listener included Factory - ### obj = Listener::create( 'FooListener' ) - ### obj = Listener::create( FooListener ) - ### obj = Listener::create( 'Foo' ) - def create( subType, *args, &block ) - subclass = getSubclass( subType ) - - return subclass.new( *args, &block ) - rescue => err - nicetrace = err.backtrace.reject {|frame| /#{__FILE__}/ =~ frame} - msg = "When creating '#{subType}': " + err.message - Kernel::raise( err.class, msg, nicetrace ) - end - - - ### Given a <tt>className</tt> like that of the first argument to - ### #create, attempt to load the corresponding class if it is not - ### already loaded and return the class object. - def getSubclass( className ) - return self if ( self.name == className || className == '' ) - return className if className.is_a?( Class ) && className >= self - - unless self.derivatives.has_key?( className.downcase ) - self.loadDerivative( className ) - - unless self.derivatives.has_key?( className.downcase ) - raise FactoryError, - "loadDerivative(%s) didn't add a '%s' key to the "\ - "registry for %s" % - [ className, className.downcase, self.name ] - end - - subclass = self.derivatives[ className.downcase ] - unless subclass.is_a?( Class ) - raise FactoryError, - "loadDerivative(%s) added something other than a class "\ - "to the registry for %s: %p" % - [ className, self.name, subclass ] - end - end - - return self.derivatives[ className.downcase ] - end - - - ### Calculates an appropriate filename for the derived class using the - ### name of the base class and tries to load it via <tt>require</tt>. If - ### the including class responds to a method named - ### <tt>derivativeDirs</tt>, its return value (either a String, or an - ### array of Strings) is added to the list of prefix directories to try - ### when attempting to require a modules. Eg., if - ### <tt>class.derivativeDirs</tt> returns <tt>['foo','bar']</tt> the - ### require line is tried with both <tt>'foo/'</tt> and <tt>'bar/'</tt> - ### prepended to it. - def loadDerivative( className ) - className = className.to_s - - #PluginFactory::log :debug, "Loading derivative #{className}" - - # Get the unique part of the derived class name and try to - # load it from one of the derivative subdirs, if there are - # any. - modName = self.getModuleName( className ) - self.requireDerivative( modName ) - - # Check to see if the specified listener is now loaded. If it - # is not, raise an error to that effect. - unless self.derivatives[ className.downcase ] - raise FactoryError, - "Couldn't find a %s named '%s'. Loaded derivatives are: %p" % [ - self.factoryType, - className.downcase, - self.derivatives.keys, - ], caller(3) - end - - return true - end - - - ### Build and return the unique part of the given <tt>className</tt> - ### either by stripping leading namespaces if the name already has the - ### name of the factory type in it (eg., 'My::FooService' for Service, - ### or by appending the factory type if it doesn't. - def getModuleName( className ) - if className =~ /\w+#{self.factoryType}/ - modName = className.sub( /(?:.*::)?(\w+)(?:#{self.factoryType})/, "\\1" ) - else - modName = className - end - - return modName - end - - - ### If the factory responds to the #derivativeDirs method, call - ### it and use the returned array as a list of directories to - ### search for the module with the specified <tt>modName</tt>. - def requireDerivative( modName ) - - # See if we have a list of special subdirs that derivatives - # live in - if ( self.respond_to?(:derivativeDirs) ) - subdirs = self.derivativeDirs - subdirs = [ subdirs ] unless subdirs.is_a?( Array ) - - # If not, just try requiring it from $LOAD_PATH - else - subdirs = [''] - end - - fatals = [] - - # Iterate over the subdirs until we successfully require a - # module. - catch( :found ) { - subdirs.collect {|dir| dir.strip}.each do |subdir| - self.makeRequirePath( modName, subdir ).each {|path| - #PluginFactory::log :debug, "Trying #{path}..." - - # Try to require the module, saving errors and jumping - # out of the catch block on success. - begin - require( path.untaint ) - rescue LoadError => err - PluginFactory::log :debug, - "No module at '%s', trying the next alternative: '%s'" % - [ path, err.message ] - rescue ScriptError,StandardError => err - fatals << err - PluginFactory::log :error, - "Found '#{path}', but encountered an error: %s\n\t%s" % - [ err.message, err.backtrace.join("\n\t") ] - else - #PluginFactory::log :debug, - # "Found '#{path}'. Throwing :found" - throw :found - end - } - end - - #PluginFactory::log :debug, "fatals = %p" % [ fatals ] - - # Re-raise is there was a file found, but it didn't load for - # some reason. - if ! fatals.empty? - #PluginFactory::log :debug, "Re-raising first fatal error" - Kernel::raise( fatals.first ) - end - - nil - } - end - - - ### Make a list of permutations of the given +modname+ for the given - ### +subdir+. Called on a +DataDriver+ class with the arguments 'Socket' and - ### 'drivers', returns: - ### ["drivers/socketdatadriver", "drivers/socketDataDriver", - ### "drivers/SocketDataDriver", "drivers/socket", "drivers/Socket"] - def makeRequirePath( modname, subdir ) - path = [] - myname = self.factoryType - - # Make permutations of the two parts - path << modname - path << modname.downcase - path << modname + myname - path << modname.downcase + myname - path << modname.downcase + myname.downcase - - # If a non-empty subdir was given, prepend it to all the items in the - # path - unless subdir.nil? or subdir.empty? - path.collect! {|m| File::join(subdir, m)} - end - - return path.uniq.reverse - end - -end # module Factory diff --git a/test/plugins/commands/test1.rb b/test/plugins/commands/test1.rb index b999d38..b8c0d89 100644 --- a/test/plugins/commands/test1.rb +++ b/test/plugins/commands/test1.rb @@ -1,19 +1,17 @@ -include Mongrel - -class First < Plugin "/commands" +class First < Mongrel::Plugin "/commands" def initialize(options = {}) puts "First with options: #{options.inspect}" end end -class Second < Plugin "/commands" +class Second < Mongrel::Plugin "/commands" def initialize(options = {}) puts "Second with options: #{options.inspect}" end end -class Last < Plugin "/commands" +class Last < Mongrel::Plugin "/commands" def initialize(options = {}) puts "Last with options: #{options.inspect}" end diff --git a/test/test_plugins.rb b/test/test_plugins.rb index 680dac5..0700e50 100644 --- a/test/test_plugins.rb +++ b/test/test_plugins.rb @@ -9,7 +9,7 @@ class PluginTest < Test::Unit::TestCase def setup @pmgr = PluginManager.instance @categories = ["/commands"] - @names = ["FirstCommand", "SecondCommand", "LastCommands"] + @names = ["/first","/second","/last"] end def test_load_plugins @@ -17,13 +17,16 @@ class PluginTest < Test::Unit::TestCase puts "#{@pmgr.available.inspect}" @pmgr.available.each {|cat,plugins| plugins.each do |p| - puts "TEST: #{cat}/#{p}" + puts "TEST: #{cat}#{p}" assert @names.include?(p) end } - @pmgr.available.each do |name| - plugin = @pmgr.create(name, options={"name" => name}) + @pmgr.available.each do |cat,plugins| + plugins.each do |p| + STDERR.puts "#{cat}#{p}" + plugin = @pmgr.create("#{cat}#{p}", options={"name" => p}) + end end end |