1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00
puma--puma/lib/puma/configurator.rb
2011-09-22 19:24:43 -07:00

388 lines
14 KiB
Ruby

require 'yaml'
require 'etc'
module Puma
# Implements a simple DSL for configuring a Puma server for your
# purposes. More used by framework implementers to setup Puma
# how they like, but could be used by regular folks to add more things
# to an existing puma configuration.
#
# It is used like this:
#
# require 'puma'
# config = Puma::Configurator.new :host => "127.0.0.1" do
# listener :port => 3000 do
# uri "/app", :handler => Puma::DirHandler.new(".", load_mime_map("mime.yaml"))
# end
# run
# end
#
# This will setup a simple DirHandler at the current directory and load additional
# mime types from mimy.yaml. The :host => "127.0.0.1" is actually not
# specific to the servers but just a hash of default parameters that all
# server or uri calls receive.
#
# When you are inside the block after Puma::Configurator.new you can simply
# call functions that are part of Configurator (like server, uri, daemonize, etc)
# without having to refer to anything else. You can also call these functions on
# the resulting object directly for additional configuration.
#
# A major thing about Configurator is that it actually lets you configure
# multiple listeners for any hosts and ports you want. These are kept in a
# map config.listeners so you can get to them.
#
# * :pid_file => Where to write the process ID.
class Configurator
attr_reader :listeners
attr_reader :defaults
attr_reader :needs_restart
# You pass in initial defaults and then a block to continue configuring.
def initialize(defaults={}, &block)
@listener = nil
@listener_name = nil
@listeners = {}
@defaults = defaults
@needs_restart = false
@pid_file = defaults[:pid_file]
if block
cloaker(&block).bind(self).call
end
end
# Change privileges of the process to specified user and group.
def change_privilege(user, group)
begin
uid, gid = Process.euid, Process.egid
target_uid = Etc.getpwnam(user).uid if user
target_gid = Etc.getgrnam(group).gid if group
if uid != target_uid or gid != target_gid
log "Initiating groups for #{user.inspect}:#{group.inspect}."
Process.initgroups(user, target_gid)
log "Changing group to #{group.inspect}."
Process::GID.change_privilege(target_gid)
log "Changing user to #{user.inspect}."
Process::UID.change_privilege(target_uid)
end
rescue Errno::EPERM => e
log "Couldn't change user and group to #{user.inspect}:#{group.inspect}: #{e.to_s}."
log "Puma failed to start."
exit 1
end
end
def remove_pid_file
File.unlink(@pid_file) if @pid_file and File.exists?(@pid_file)
end
# Writes the PID file if we're not on Windows.
def write_pid_file
unless RbConfig::CONFIG['host_os'] =~ /mingw|mswin/
log "Writing PID file to #{@pid_file}"
open(@pid_file,"w") {|f| f.write(Process.pid) }
open(@pid_file,"w") do |f|
f.write(Process.pid)
File.chmod(0644, @pid_file)
end
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(&block)
cloaking_class.class_eval do
define_method :cloaker_, &block
meth = instance_method( :cloaker_ )
remove_method :cloaker_
meth
end
end
# This will resolve the given options against the defaults.
# Normally just used internally.
def resolve_defaults(options)
options.merge(@defaults)
end
# Starts a listener block. This is the only one that actually takes
# a block and then you make Configurator.uri calls in order to setup
# your URIs and handlers. If you write your Handlers as GemPlugins
# then you can use load_plugins and plugin to load them.
#
# It expects the following options (or defaults):
#
# * :host => Host name to bind.
# * :port => Port to bind.
# * :num_processors => The maximum number of concurrent threads allowed.
# * :throttle => Time to pause (in hundredths of a second) between accepting clients.
# * :timeout => Time to wait (in seconds) before killing a stalled thread.
# * :user => User to change to, must have :group as well.
# * :group => Group to change to, must have :user as well.
#
def listener(options={},&block)
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_name = "#{ops[:host]}:#{ops[:port]}"
@listeners[@listener_name] = @listener
if ops[:user] and ops[:group]
change_privilege(ops[:user], ops[:group])
end
# Does the actual cloaking operation to give the new implicit self.
if block
cloaker(&block).bind(self).call
end
# all done processing this listener setup, reset implicit variables
@listener = nil
@listener_name = nil
end
# Called inside a Configurator.listener block in order to
# add URI->handler mappings for that listener. Use this as
# many times as you like. It expects the following options
# or defaults:
#
# * :handler => HttpHandler -- Handler to use for this location.
# * :in_front => true/false -- Rather than appending, it prepends this handler.
def uri(location, options={})
ops = resolve_defaults(options)
@listener.register(location, ops[:handler], ops[:in_front])
end
# Daemonizes the current Ruby script turning all the
# listeners into an actual "server" or detached process.
# You must call this *before* frameworks that open files
# as otherwise the files will be closed by this function.
#
# Does not work for Win32 systems (the call is silently ignored).
#
# Requires the following options or defaults:
#
# * :cwd => Directory to change to.
# * :log_file => Where to write STDOUT and STDERR.
#
# It is safe to call this on win32 as it will only require the daemons
# gem/library if NOT win32.
def daemonize(options={})
ops = resolve_defaults(options)
# save this for later since daemonize will hose it
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
# or exclude from the determining the dependencies.
def load_plugins(options={})
ops = resolve_defaults(options)
load_settings = {}
if ops[:includes]
ops[:includes].each do |plugin|
load_settings[plugin] = GemPlugin::INCLUDE
end
end
if ops[:excludes]
ops[:excludes].each do |plugin|
load_settings[plugin] = GemPlugin::EXCLUDE
end
end
GemPlugin::Manager.instance.load(load_settings)
end
# Easy way to load a YAML file and apply default settings.
def load_yaml(file, default={})
default.merge(YAML.load_file(file))
end
# Loads the MIME map file and checks that it is correct
# on loading. This is commonly passed to 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.
def plugin(name, options={})
ops = resolve_defaults(options)
GemPlugin::Manager.instance.create(name, ops)
end
# Lets you do redirects easily as described in Puma::RedirectHandler.
# You use it inside the configurator like this:
#
# redirect("/test", "/to/there") # simple
# redirect("/to", /t/, 'w') # regexp
# redirect("/hey", /(w+)/) {|match| ...} # block
#
def redirect(from, pattern, replacement = nil, &block)
uri from, :handler => Puma::RedirectHandler.new(pattern, replacement, &block)
end
# Works like a meta run method which goes through all the
# configured listeners. Use the Configurator.join method
# to prevent Ruby from exiting until each one is done.
def run
@listeners.each {|name,s|
s.run
}
$puma_sleeper_thread = Thread.new { loop { sleep 1 } }
end
# Calls .stop on all the configured listeners so they
# stop processing requests (gracefully). By default it
# assumes that you don't want to restart.
def stop(needs_restart=false, synchronous=false)
@listeners.each do |name,s|
s.stop(synchronous)
end
@needs_restart = needs_restart
end
# This method should actually be called *outside* of the
# Configurator block so that you can control it. In other words
# do it like: config.join.
def join
@listeners.values.each {|s| s.acceptor.join }
end
# Calling this before you register your URIs to the given location
# will setup a set of handlers that log open files, objects, and the
# parameters for each request. This helps you track common problems
# found in Rails applications that are either slow or become unresponsive
# after a little while.
#
# You can pass an extra parameter *what* to indicate what you want to
# debug. For example, if you just want to dump rails stuff then do:
#
# debug "/", what = [:rails]
#
# And it will only produce the log/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
# binding so they can put their own Configurator statements.
def run_config(script)
open(script) {|f| eval(f.read, proc {self}.binding) }
end
# Sets up the standard signal handlers that are used on most Ruby
# It only configures if the platform is not win32 and doesn't do
# a HUP signal since this is typically framework specific.
#
# Requires a :pid_file option given to Configurator.new to indicate a file to delete.
# It sets the PumaConfig.needs_restart attribute if
# the start command should reload. It's up to you to detect this
# and do whatever is needed for a "restart".
#
# This command is safely ignored if the platform is win32 (with a warning)
def setup_signals(options={})
ops = resolve_defaults(options)
# forced shutdown, even if previously restarted (actually just like TERM but for CTRL-C)
trap("INT") { log "INT signal received."; stop(false) }
# clean up the pid file always
at_exit { remove_pid_file }
unless RbConfig::CONFIG['host_os'] =~ /mingw|mswin/
# graceful shutdown
trap("TERM") { log "TERM signal received."; stop }
trap("USR1") { log "USR1 received, toggling $puma_debug_client to #{!$puma_debug_client}"; $puma_debug_client = !$puma_debug_client }
# restart
trap("USR2") { log "USR2 signal received."; stop(true) }
log "Signals ready. TERM => stop. USR2 => restart. INT => stop (no restart)."
else
log "Signals ready. INT => stop (no restart)."
end
end
# Logs a simple message to STDERR (or the puma log if in daemon mode).
def log(msg)
STDERR.print "** ", msg, "\n"
end
end
end