From 052c456d81378eab33397c49455bf046c874a680 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 23 Sep 2011 20:24:32 -0700 Subject: [PATCH] Runs a rack app! --- Manifest.txt | 2 +- bin/{puma_rails => puma} | 121 +++++----------- lib/puma/command.rb | 33 +++-- lib/puma/configurator.rb | 123 ++-------------- lib/puma/gem_plugin.rb | 302 +++++++++++++++++++++++++++++++++++++++ lib/puma/init.rb | 2 +- tasks/gem.rake | 2 +- test/lobster.ru | 4 + 8 files changed, 374 insertions(+), 215 deletions(-) rename bin/{puma_rails => puma} (56%) create mode 100644 lib/puma/gem_plugin.rb create mode 100644 test/lobster.ru diff --git a/Manifest.txt b/Manifest.txt index 5a96beab..685fcbfe 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -5,7 +5,7 @@ Manifest.txt README.txt Rakefile TODO -bin/puma_rails +bin/puma examples/builder.rb examples/camping/README examples/camping/blog.rb diff --git a/bin/puma_rails b/bin/puma similarity index 56% rename from bin/puma_rails rename to bin/puma index 976b551c..462fde30 100644 --- a/bin/puma_rails +++ b/bin/puma @@ -9,11 +9,9 @@ require 'yaml' require 'etc' -$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib" require 'puma' -require 'puma/rails' -Puma::Gems.require 'gem_plugin' +Puma::Gems.require 'puma/gem_plugin' # require 'ruby-debug' # Debugger.start @@ -24,25 +22,19 @@ module Puma def configure options [ - ["-e", "--environment ENV", "Rails environment to run as", :@environment, ENV['RAILS_ENV'] || "development"], - ["-d", "--daemonize", "Run daemonized in the background", :@daemon, false], ['-p', '--port PORT', "Which port to bind to", :@port, 3000], ['-a', '--address ADDR', "Address to bind to", :@address, "0.0.0.0"], ['-l', '--log FILE', "Where to write log messages", :@log_file, "log/puma.log"], ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/puma.pid"], - ['-n', '--num-processors INT', "Number of processors active before clients denied", :@num_processors, 1024], - ['-o', '--timeout TIME', "Time to wait (in seconds) before killing a stalled thread", :@timeout, 60], - ['-t', '--throttle TIME', "Time to pause (in hundredths of a second) between accepting clients", :@throttle, 0], - ['-m', '--mime PATH', "A YAML file that lists additional MIME types", :@mime_map, nil], + ['-n', '--concurrency INT', "Number of concurrent threads to use", + :@concurrency, 16], ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd], - ['-r', '--root PATH', "Set the document root (default 'public')", :@docroot, "public"], - ['-B', '--debug', "Enable debugging mode", :@debug, false], ['-C', '--config PATH', "Use a config file", :@config_file, nil], ['-S', '--script PATH', "Load the given file as an extra config script", :@config_script, nil], ['-G', '--generate PATH', "Generate a config file for use with -C", :@generate, nil], + ['-r', '--rackup PATH', 'Load a specific rackup file', :@rackup_file, "config.ru"], ['', '--user USER', "User to run as", :@user, nil], - ['', '--group GROUP', "Group to run as", :@group, nil], - ['', '--prefix PATH', "URL prefix for Rails app", :@prefix, nil] + ['', '--group GROUP', "Group to run as", :@group, nil] ] end @@ -61,99 +53,52 @@ module Puma # Change there to start, then we'll have to come back after daemonize Dir.chdir(@cwd) - valid?(@prefix[0] == ?/ && @prefix[-1] != ?/, "Prefix must begin with / and not end in /") if @prefix valid_dir? File.dirname(@log_file), "Path to log file not valid: #@log_file" valid_dir? File.dirname(@pid_file), "Path to pid file not valid: #@pid_file" - valid_dir? @docroot, "Path to docroot not valid: #@docroot" valid_exists? @mime_map, "MIME mapping file does not exist: #@mime_map" if @mime_map valid_exists? @config_file, "Config file not there: #@config_file" if @config_file valid_dir? File.dirname(@generate), "Problem accessing directory to #@generate" if @generate valid_user? @user if @user valid_group? @group if @group + @rackup = ARGV.shift || "config.ru" + + valid? File.exists?(@rackup), "Unable to find rackup file" + return @valid end def run if @generate - @generate = File.expand_path(@generate) + @generate = File.expand_path(@generate) @stderr.puts "** Writing config to \"#@generate\"." open(@generate, "w") {|f| f.write(settings.to_yaml) } @stderr.puts "** Finished. Run \"puma_rails start -C #@generate\" to use the config file." exit 0 end - config = Puma::Rails::RailsConfigurator.new(settings) do - if defaults[:daemon] - if File.exist? defaults[:pid_file] - log "!!! PID file #{defaults[:pid_file]} already exists. Puma could be running already. Check your #{defaults[:log_file]} for errors." - log "!!! Exiting with error. You must stop puma and clear the .pid before I'll attempt a start." - exit 1 + rackup = @rackup + + config = Puma::Configurator.new(settings) do |c| + c.log "Starting Puma listening at #{c.defaults[:host]}:#{c.defaults[:port]}" + + c.listener do |l| + l.load_rackup rackup + + l.load_plugins + + if c.defaults[:config_script] + c.log "Loading #{c.defaults[:config_script]} external config script" + c.run_config(c.defaults[:config_script]) end - - daemonize - write_pid_file - log "Daemonized, any open files are closed. Look at #{defaults[:pid_file]} and #{defaults[:log_file]} for info." - log "Settings loaded from #{@config_file} (they override command line)." if @config_file - end - - log "Starting Puma listening at #{defaults[:host]}:#{defaults[:port]}" - - 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/puma_debug for the files." - debug "/" - end - - log "Starting Rails with #{defaults[:environment]} environment..." - log "Mounting Rails at #{defaults[:prefix]}..." if defaults[:prefix] - uri defaults[:prefix] || "/", :handler => rails(:mime => mime, :prefix => defaults[:prefix]) - log "Rails loaded." - - log "Loading any Rails specific GemPlugins" - load_plugins - - if defaults[:config_script] - log "Loading #{defaults[:config_script]} external config script" - run_config(defaults[:config_script]) - end - - setup_rails_signals end end config.run config.log "Puma #{Puma::Const::PUMA_VERSION} available at #{@address}:#{@port}" - - unless config.defaults[:daemon] - config.log "Use CTRL-C to stop." - end + config.log "Use CTRL-C to stop." config.join - - if config.needs_restart - unless RbConfig::CONFIG['host_os'] =~ /mingw|mswin/ - cmd = "ruby #{__FILE__} start #{original_args.join(' ')}" - config.log "Restarting with arguments: #{cmd}" - config.stop(false, true) - config.remove_pid_file - - if config.defaults[:daemon] - system cmd - else - @stderr.puts "Can't restart unless in daemon mode." - exit 1 - end - else - config.log "Win32 does not support restarts. Exiting." - end - end end def load_config @@ -161,7 +106,7 @@ module Puma begin settings = YAML.load_file(@config_file) ensure - @stderr.puts "** Loading settings from #{@config_file} (they override command line)." unless @daemon || settings[:daemon] + @stderr.puts "** Loading settings from #{@config_file} (they override command line)." end settings[:includes] ||= ["puma"] @@ -181,7 +126,7 @@ module Puma def config_keys @config_keys ||= - %w(address host port cwd log_file pid_file environment docroot mime_map daemon debug includes config_script num_processors timeout throttle user group prefix) + %w(address host port cwd log_file pid_file config_script concurrency user group) end def settings @@ -194,7 +139,7 @@ module Puma end end - def Puma::send_signal(signal, pid_file) + def Puma.send_signal(signal, pid_file) pid = File.read(pid_file).to_i print "Sending #{signal} to Puma at PID #{pid}..." begin @@ -206,7 +151,6 @@ module Puma puts "Done." end - class Stop < GemPlugin::Plugin "/commands" include Puma::Command::Base @@ -236,9 +180,9 @@ module Puma sleep 1 end - Puma::send_signal("KILL", @pid_file) if File.exist? @pid_file + Puma.send_signal("KILL", @pid_file) if File.exist? @pid_file else - Puma::send_signal("TERM", @pid_file) + Puma.send_signal("TERM", @pid_file) end end end @@ -267,16 +211,15 @@ module Puma def run if @soft - Puma::send_signal("HUP", @pid_file) + Puma.send_signal("HUP", @pid_file) else - Puma::send_signal("USR2", @pid_file) + Puma.send_signal("USR2", @pid_file) end end end end -GemPlugin::Manager.instance.load "puma" => GemPlugin::INCLUDE, - "rails" => GemPlugin::EXCLUDE +GemPlugin::Manager.instance.load "puma" => GemPlugin::INCLUDE exit 1 unless Puma::Command::Registry.instance.run(ARGV) diff --git a/lib/puma/command.rb b/lib/puma/command.rb index a90c9a53..bae553ef 100644 --- a/lib/puma/command.rb +++ b/lib/puma/command.rb @@ -7,7 +7,7 @@ require 'singleton' require 'optparse' require 'puma/gems' -Puma::Gems.require 'gem_plugin' +Puma::Gems.require 'puma/gem_plugin' module Puma @@ -16,7 +16,7 @@ module Puma module Command - BANNER = "Usage: puma_rails [options]" + BANNER = "Usage: puma [options]" # A Command pattern implementation used to create the set of command available to the user # from Puma. The script uses objects which implement this interface to do the @@ -31,9 +31,10 @@ module Puma def options(opts) # process the given options array opts.each do |short, long, help, variable, default| - self.instance_variable_set(variable, default) + instance_variable_set(variable, default) + @opt.on(short, long, help) do |arg| - self.instance_variable_set(variable, arg) + instance_variable_set(variable, arg) end end end @@ -43,7 +44,7 @@ module Puma def initialize(options={}) argv = options[:argv] || [] @stderr = options[:stderr] || $stderr - @stdout = options[:stdout] || $stderr + @stdout = options[:stdout] || $stdout @opt = OptionParser.new @opt.banner = Puma::Command::BANNER @@ -165,22 +166,30 @@ module Puma @stdout.puts "#{Puma::Command::BANNER}\nAvailable commands are:\n\n" self.commands.each do |name| - if /puma::/ =~ name - name = name[9 .. -1] + if /puma::(.*)/ =~ name + name = $1 end - @stdout.puts " - #{name[1 .. -1]}\n" + @stdout.puts " - #{name}\n" end @stdout.puts "\nEach command takes -h as an option to get help." end + BUILTIN_COMMANDS = ["start", "stop", "restart"] # 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 - cmd_name = args.shift + cmd_name = args.first + + if !cmd_name or (!BUILTIN_COMMANDS.include?(cmd_name) and + File.exists?(cmd_name)) + cmd_name = "start" + else + args.shift + end if !cmd_name or cmd_name == "?" or cmd_name == "help" print_command_list @@ -191,8 +200,10 @@ module Puma end begin - # quick hack so that existing commands will keep working but the Puma:: ones can be moved - if ["start", "stop", "restart"].include? cmd_name + # quick hack so that existing commands will keep working but the + # Puma:: ones can be moved + + if BUILTIN_COMMANDS.include? cmd_name cmd_name = "puma::" + cmd_name end diff --git a/lib/puma/configurator.rb b/lib/puma/configurator.rb index 3306f715..e68b2abb 100644 --- a/lib/puma/configurator.rb +++ b/lib/puma/configurator.rb @@ -1,6 +1,8 @@ require 'yaml' require 'etc' +require 'rack/builder' + module Puma # Implements a simple DSL for configuring a Puma server for your # purposes. More used by framework implementers to setup Puma @@ -47,7 +49,7 @@ module Puma @pid_file = defaults[:pid_file] if block - cloaker(&block).bind(self).call + yield self end end @@ -129,14 +131,14 @@ module Puma # * :user => User to change to, must have :group as well. # * :group => Group to change to, must have :user as well. # - def listener(options={},&block) + def listener(options={}) raise "Cannot call listener inside another listener block." if (@listener or @listener_name) ops = resolve_defaults(options) ops[:num_processors] ||= 950 ops[:throttle] ||= 0 ops[:timeout] ||= 60 - @listener = Puma::HttpServer.new(ops[:host], ops[:port].to_i, ops[:num_processors].to_i, ops[:throttle].to_i, ops[:timeout].to_i) + @listener = Puma::Server.new(ops[:host], ops[:port].to_i, ops[:concurrency].to_i) @listener_name = "#{ops[:host]}:#{ops[:port]}" @listeners[@listener_name] = @listener @@ -145,8 +147,8 @@ module Puma end # Does the actual cloaking operation to give the new implicit self. - if block - cloaker(&block).bind(self).call + if block_given? + yield self end # all done processing this listener setup, reset implicit variables @@ -154,60 +156,13 @@ module Puma @listener_name = nil end + def load_rackup(file) + app, options = Rack::Builder.parse_file file - # 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], ops[:in_front]) + @listener.app = app + # Do something with options? 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 - unless RbConfig::CONFIG['host_os'] =~ /mingw|mswin/ - require 'daemons/daemonize' - - logfile = ops[:log_file] - if logfile[0].chr != "/" - logfile = File.join(ops[:cwd],logfile) - if not File.exist?(File.dirname(logfile)) - log "!!! Log file directory not found at full path #{File.dirname(logfile)}. Update your configuration to use a full path." - exit 1 - end - end - - Daemonize.daemonize(logfile) - - # 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 @@ -237,24 +192,6 @@ module Puma 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 Puma::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 Puma::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. @@ -281,8 +218,6 @@ module Puma @listeners.each {|name,s| s.run } - - $puma_sleeper_thread = Thread.new { loop { sleep 1 } } end # Calls .stop on all the configured listeners so they @@ -303,42 +238,6 @@ module Puma @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/puma_debug/rails.log file. - # Available options are: :access, :files, :objects, :threads, :rails - # - # NOTE: Use [:files] to get accesses dumped to stderr like with WEBrick. - def debug(location, what = [:access, :files, :objects, :threads, :rails]) - require 'puma/debug' - handlers = { - :access => "/handlers/requestlog::access", - :files => "/handlers/requestlog::files", - :objects => "/handlers/requestlog::objects", - :threads => "/handlers/requestlog::threads", - :rails => "/handlers/requestlog::params" - } - - # turn on the debugging infrastructure, and ObjectTracker is a pig - PumaDbg.configure - - # now we roll through each requested debug type, turn it on and load that plugin - what.each do |type| - PumaDbg.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 diff --git a/lib/puma/gem_plugin.rb b/lib/puma/gem_plugin.rb new file mode 100644 index 00000000..13d71eda --- /dev/null +++ b/lib/puma/gem_plugin.rb @@ -0,0 +1,302 @@ +require 'singleton' +require 'rubygems' + +# Implements a dynamic plugin loading, configuration, and discovery system +# based on RubyGems and a simple additional name space that looks like a URI. +# +# A plugin is created and put into a category with the following code: +# +# class MyThing < GemPlugin::Plugin "/things" +# ... +# end +# +# What this does is sets up your MyThing in the plugin registry via GemPlugin::Manager. +# You can then later get this plugin with GemPlugin::Manager.create("/things/mything") +# and can also pass in options as a second parameter. +# +# This isn't such a big deal, but the power is really from the GemPlugin::Manager.load +# method. This method will go through the installed gems and require_gem any +# that depend on the gem_plugin RubyGem. You can arbitrarily include or exclude +# gems based on what they also depend on, thus letting you load these gems when appropriate. +# +# Since this system was written originally for the Mongrel project that'll be the +# best example of using it. +# +# Imagine you have a neat plugin for Mongrel called snazzy_command that gives the +# mongrel_rails a new command snazzy (like: mongrel_rails snazzy). You'd like +# people to be able to grab this plugin if they want and use it, because it's snazzy. +# +# First thing you do is create a gem of your project and make sure that it depends +# on "mongrel" AND "gem_plugin". This signals to the GemPlugin system that this is +# a plugin for mongrel. +# +# Next you put this code into a file like lib/init.rb (can be anything really): +# +# class Snazzy < GemPlugin::Plugin "/commands" +# ... +# end +# +# Then when you create your gem you have the following bits in your Rakefile: +# +# spec.add_dependency('mongrel', '>= 0.3.9') +# spec.add_dependency('gem_plugin', '>= 0.1') +# spec.autorequire = 'init.rb' +# +# Finally, you just have to now publish this gem for people to install and Mongrel +# will "magically" be able to install it. +# +# The "magic" part though is pretty simple and done via the GemPlugin::Manager.load +# method. Read that to see how it is really done. +module GemPlugin + + EXCLUDE = true + INCLUDE = false + + class PluginNotLoaded < StandardError; end + + + # This class is used by people who use gem plugins (but don't necessarily make them) + # to add plugins to their own systems. It provides a way to load plugins, list them, + # and create them as needed. + # + # It is a singleton so you use like this: GemPlugins::Manager.instance.load + class Manager + include Singleton + attr_reader :plugins + attr_reader :gems + + + def initialize + # plugins that have been loaded + @plugins = {} + + # keeps track of gems which have been loaded already by the manager *and* + # where they came from so that they can be referenced later + @gems = {} + end + + def every_gem + if Gem::VERSION >= '1.8.0' + gems = Gem::Specification.to_a + gems.each { |g| yield g } + else + gems = Gem::SourceIndex.from_gems_in(sdirs) + gems.each do |path, gem| + yield gem + end + end + end + + # Responsible for going through the list of available gems and loading + # any plugins requested. It keeps track of what it's loaded already + # and won't load them again. + # + # It accepts one parameter which is a hash of gem depends that should include + # or exclude a gem from being loaded. A gem must depend on gem_plugin to be + # considered, but then each system has to add it's own INCLUDE to make sure + # that only plugins related to it are loaded. + # + # An example again comes from Mongrel. In order to load all Mongrel plugins: + # + # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE + # + # Which will load all plugins that depend on mongrel AND gem_plugin. Now, one + # extra thing we do is we delay loading Rails Mongrel plugins until after rails + # is configured. Do do this the mongrel_rails script has: + # + # GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE + # The only thing to remember is that this is saying "include a plugin if it + # depends on gem_plugin, mongrel, but NOT rails". If a plugin also depends on other + # stuff then it's loaded just fine. Only gem_plugin, mongrel, and rails are + # ever used to determine if it should be included. + # + # NOTE: Currently RubyGems will run autorequire on gems that get required AND + # on their dependencies. This really messes with people running edge rails + # since activerecord or other stuff gets loaded for just touching a gem plugin. + # To prevent this load requires the full path to the "init.rb" file, which + # avoids the RubyGems autorequire magic. + def load(needs = {}) + needs = needs.merge({"gem_plugin" => INCLUDE}) + + every_gem do |gem| + # don't load gems more than once + next if @gems.has_key? gem.name + check = needs.dup + + # rolls through the depends and inverts anything it finds + gem.dependencies.each do |dep| + # this will fail if a gem is depended more than once + if check.has_key? dep.name + check[dep.name] = !check[dep.name] + end + end + + # now since excluded gems start as true, inverting them + # makes them false so we'll skip this gem if any excludes are found + if (check.select {|name,test| !test}).length == 0 + # looks like no needs were set to false, so it's good + + # Previously was set wrong, we already have the correct gem path! + gem_dir = "" + Gem.path.each do |gem_path| + gem_dir = File.join(gem_path, "gems", path) + break if File.exists?(gem_dir) + end + + require File.join(gem_dir, "lib", gem.name, "init.rb") + @gems[gem.name] = gem_dir + end + end + + return nil + end + + + # Not necessary for you to call directly, but this is + # how GemPlugin::Base.inherited actually adds a + # plugin to a category. + def register(category, name, klass) + @plugins[category] ||= {} + @plugins[category][name.downcase] = klass + end + + + # Resolves the given name (should include /category/name) to + # find the plugin class and create an instance. You can + # pass a second hash option that is then given to the Plugin + # to configure it. + def create(name, options = {}) + last_slash = name.rindex("/") + category = name[0 ... last_slash] + plugin = name[last_slash .. -1] + + map = @plugins[category] + if not map + raise "Plugin category #{category} does not exist" + elsif not map.has_key? plugin + raise "Plugin #{plugin} does not exist in category #{category}" + else + map[plugin].new(options) + end + end + + + # Simply says whether the given gem has been loaded yet or not. + def loaded?(gem_name) + @gems.has_key? gem_name + end + + + # GemPlugins can have a 'resources' directory which is packaged with them + # and can hold any data resources the plugin may need. The main problem + # is that it's difficult to figure out where these resources are + # actually located on the file system. The resource method tries to + # locate the real path for a given resource path. + # + # Let's say you have a 'resources/stylesheets/default.css' file in your + # gem distribution, then finding where this file really is involves: + # + # Manager.instance.resource("mygem", "/stylesheets/default.css") + # + # You either get back the full path to the resource or you get a nil + # if it doesn't exist. + # + # If you request a path for a GemPlugin that hasn't been loaded yet + # then it will throw an PluginNotLoaded exception. The gem may be + # present on your system in this case, but you just haven't loaded + # it with Manager.instance.load properly. + def resource(gem_name, path) + if not loaded? gem_name + raise PluginNotLoaded.new("Plugin #{gem_name} not loaded when getting resource #{path}") + end + + file = File.join(@gems[gem_name], "resources", path) + + if File.exist? file + return file + else + return nil + end + end + + + # While Manager.resource will find arbitrary resources, a special + # case is when you need to load a set of configuration defaults. + # GemPlugin normalizes this to be if you have a file "resources/defaults.yaml" + # then you'll be able to load them via Manager.config. + # + # How you use the method is you get the options the user wants set, pass + # them to Manager.instance.config, and what you get back is a new Hash + # with the user's settings overriding the defaults. + # + # opts = Manager.instance.config "mygem", :age => 12, :max_load => .9 + # + # In the above case, if defaults had {:age => 14} then it would be + # changed to 12. + # + # This loads the .yaml file on the fly every time, so doing it a + # whole lot is very stupid. If you need to make frequent calls to + # this, call it once with no options (Manager.instance.config) then + # use the returned defaults directly from then on. + def config(gem_name, options={}) + config_file = Manager.instance.resource(gem_name, "/defaults.yaml") + if config_file + begin + defaults = YAML.load_file(config_file) + return defaults.merge(options) + rescue + raise "Error loading config #{config_file} for gem #{gem_name}" + end + else + return options + end + end + end + + # This base class for plugins really does nothing + # more than wire up the new class into the right category. + # It is not thread-safe yet but will be soon. + class Base + + attr_reader :options + + # See Mongrel::Plugin for an explanation. + def Base.inherited(klass) + name = "/" + klass.to_s.downcase + Manager.instance.register(@@category, name, klass) + @@category = nil + end + + # See Mongrel::Plugin for an explanation. + def Base.category=(category) + @@category = category + end + + def initialize(options = {}) + @options = options + end + + end + + # This nifty function works with the GemPlugin::Base to give you + # the syntax: + # + # class MyThing < GemPlugin::Plugin "/things" + # ... + # end + # + # What it does is temporarily sets the GemPlugin::Base.category, and then + # returns GemPlugin::Base. Since the next immediate thing Ruby does is + # use this returned class to create the new class, GemPlugin::Base.inherited + # gets called. GemPlugin::Base.inherited then uses the set category, class name, + # and class to register the plugin in the right way. + def GemPlugin.Plugin(c) + Base.category = c + Base + end + +end + + + + diff --git a/lib/puma/init.rb b/lib/puma/init.rb index 24c1ccd2..2ae0bb9c 100644 --- a/lib/puma/init.rb +++ b/lib/puma/init.rb @@ -3,6 +3,6 @@ # require 'puma/gems' -Puma::Gems.require 'gem_plugin' +Puma::Gems.require 'puma/gem_plugin' # File is just a stub that makes sure the puma_plugins gem is loaded and ready diff --git a/tasks/gem.rake b/tasks/gem.rake index 6651a640..4fdb061d 100644 --- a/tasks/gem.rake +++ b/tasks/gem.rake @@ -5,7 +5,7 @@ HOE = Hoe.spec 'puma' do developer 'Evan Phoenix', 'evan@phx.io' spec_extras[:extensions] = ["ext/http11/extconf.rb"] - spec_extras[:executables] = ['puma_rails'] + spec_extras[:executables] = ['puma'] extra_rdoc_files << 'LICENSE' diff --git a/test/lobster.ru b/test/lobster.ru new file mode 100644 index 00000000..cc7ffcae --- /dev/null +++ b/test/lobster.ru @@ -0,0 +1,4 @@ +require 'rack/lobster' + +use Rack::ShowExceptions +run Rack::Lobster.new