diff options
-rw-r--r-- | lib/mongrel/command.rb | 201 | ||||
-rw-r--r-- | lib/pluginfactory.rb | 385 |
2 files changed, 586 insertions, 0 deletions
diff --git a/lib/mongrel/command.rb b/lib/mongrel/command.rb new file mode 100644 index 0000000..9d73dad --- /dev/null +++ b/lib/mongrel/command.rb @@ -0,0 +1,201 @@ +require 'singleton' +require 'optparse' +require 'pluginfactory' + + +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. + # + # Implementing a command is fairly easy. Refer to some of the stock commands in the + # lib/mongrel/command directory for examples. + class Command + include PluginFactory + + attr_reader :valid, :done_validating + + # Called by the implemented command to set the options for that command. + # Every option has a short and long version, a description, a variable to + # set, and a default value. No exceptions. + def options(opts) + # process the given options array + opts.each do |short, long, help, variable, default| + self.instance_variable_set(variable, default) + @opt.on(short, long, help) do |arg| + self.instance_variable_set(variable, arg) + end + end + end + + # Called by the subclass to setup the command and parse the argv arguments. + # The call is destructive on argv since it uses the OptionParser#parse! function. + def initialize(argv) + @opt = OptionParser.new + @valid = true + # this is retarded, but it has to be done this way because -h and -v exit + @done_validating = false + + configure + + # I need to add my own -h definition to prevent the -h by default from exiting. + @opt.on_tail("-h", "--help", "Show this message") do + @done_validating = true + puts @opt + end + + # I need to add my own -v definition to prevent the -h from exiting by default as well. + @opt.on_tail("--version", "Show version") do + @done_validating = true + puts "No version yet." + end + + @opt.parse! argv + end + + # Tells the PluginFactory where to look for additional commands. By default + # it's just a "mongrel" directory wherever we are located. + def self.derivativeDirs + return ["mongrel"] + end + + # Returns true/false depending on whether the command is configured properly. + def validate + return @valid + end + + # Returns a help message. Defaults to OptionParser#help which should be good. + def help + @opt.help + end + + # Runs the command doing it's job. You should implement this otherwise it will + # throw a NotImplementedError as a reminder. + def run + raise NotImplementedError + end + + + # Validates the given expression is true and prints the message if not, exiting. + def valid?(exp, message) + if not @done_validating and (not exp) + STDERR.puts message + @valid = false + @done_validating = true + end + end + + # Validates that a file exists and if not displays the message + def valid_exists?(file, message) + valid?(file != nil && File.exist?(file), message) + end + + + # Validates that the file is a file and not a directory or something else. + def valid_file?(file, message) + valid?(file != nil && File.file?(file), message) + end + + # Validates that the given directory exists + def valid_dir?(file, message) + valid?(file != nil && File.directory?(file), message) + end + end + + + + # A Singleton class that manages all of the available commands + # and handles running them. + class Registry + include Singleton + + # 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 unless match.match(key.to_s) + end + + return results.sort + end + + # Prints a list of available commands. + def print_command_list + puts "Available commands are:\n" + + self.commands.each do |name| + puts " - #{name}\n" + end + + puts "Each command takes -h as an option to get help." + + end + + + # Runs the args against the first argument as the command name. + # If it has any errors it returns a false, otherwise it return true. + def run(args) + # find the command and change the program's name to reflect it + cmd_name = args.shift + $0 = "#{cmd_name}" + + if cmd_name == "?" or cmd_name == "help" + print_command_list + return true + end + + # command exists, set it up and validate it + begin + command = Command.create(cmd_name, args) + rescue FactoryError + STDERR.puts :command, "INVALID COMMAND." + print_command_list + return + end + + # Normally the command is NOT valid right after being created + # but sometimes (like with -h or -v) there's no further processing + # needed so the command is already valid so we can skip it. + if not command.done_validating + if not command.validate + STDERR.puts :command, "#{cmd_name} reported an error. Use -h to get help." + return false + else + command.run + end + end + return true + end + + # Runs the command like normal, but redirects $stdout and $stderr to the + # requested log file (which should be a file like object opened by you). + # It also marks the start and end times in the log file. + def run_redirect(log, args) + res = false + + begin + oldstdout = $stdout + oldstderr = $stderr + + log.write ">>>>>> #{Time.now}\n" + $stdout = log + $stderr = log + + res = run(args) + + log.write "<<<<<< #{Time.now}\n" + + ensure + $stdout = oldstdout + $stderr = oldstderr + return res + end + end + end + end +end + diff --git a/lib/pluginfactory.rb b/lib/pluginfactory.rb new file mode 100644 index 0000000..41576e8 --- /dev/null +++ b/lib/pluginfactory.rb @@ -0,0 +1,385 @@ +#!/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> +# +#:include: COPYRIGHT +# +#--- +# +# 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 |