diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index 760eb70f54..7d747d9c5c 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -47,21 +47,21 @@ def initialize(argv, events=Events.stdio) @parser.parse! @argv if file = @argv.shift - @conf.configure do |c| - c.rackup file + @conf.configure do |user_config, file_config| + file_config.rackup file end end rescue UnsupportedOption exit 1 end - @conf.configure do |c| + @conf.configure do |user_config, file_config| if @stdout || @stderr - c.stdout_redirect @stdout, @stderr, @append + user_config.stdout_redirect @stdout, @stderr, @append end if @control_url - c.activate_control_app @control_url, @control_options + user_config.activate_control_app @control_url, @control_options end end @@ -87,14 +87,14 @@ def unsupported(str) # def setup_options - @conf = Configuration.new do |c| + @conf = Configuration.new do |user_config, file_config| @parser = OptionParser.new do |o| o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg| - c.bind arg + user_config.bind arg end o.on "-C", "--config PATH", "Load PATH as a config file" do |arg| - c.load arg + file_config.load arg end o.on "--control URL", "The bind url to use for the control server", @@ -112,21 +112,21 @@ def setup_options end o.on "-d", "--daemon", "Daemonize the server into the background" do - c.daemonize - c.quiet + user_config.daemonize + user_config.quiet end o.on "--debug", "Log lowlevel debugging information" do - c.debug + user_config.debug end o.on "--dir DIR", "Change to DIR before starting" do |d| - c.directory d + user_config.directory d end o.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default development)" do |arg| - c.environment arg + user_config.environment arg end o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg| @@ -135,50 +135,50 @@ def setup_options o.on "-p", "--port PORT", "Define the TCP port to bind to", "Use -b for more advanced options" do |arg| - c.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}" + user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}" end o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg| - c.pidfile arg + user_config.pidfile arg end o.on "--preload", "Preload the app. Cluster mode only" do - c.preload_app! + user_config.preload_app! end o.on "--prune-bundler", "Prune out the bundler env if possible" do - c.prune_bundler + user_config.prune_bundler end o.on "-q", "--quiet", "Do not log requests internally (default true)" do - c.quiet + user_config.quiet end o.on "-v", "--log-requests", "Log requests as they occur" do - c.log_requests + user_config.log_requests end o.on "-R", "--restart-cmd CMD", "The puma command to run during a hot restart", "Default: inferred" do |cmd| - c.restart_command cmd + user_config.restart_command cmd end o.on "-S", "--state PATH", "Where to store the state details" do |arg| - c.state_path arg + user_config.state_path arg end o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg| min, max = arg.split(":") if max - c.threads min, max + user_config.threads min, max else - c.threads min, min + user_config.threads min, min end end o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do - c.tcp_mode! + user_config.tcp_mode! end o.on "-V", "--version", "Print the version information" do @@ -188,11 +188,11 @@ def setup_options o.on "-w", "--workers COUNT", "Activate cluster mode: How many worker processes to create" do |arg| - c.workers arg + user_config.workers arg end o.on "--tag NAME", "Additional text to display in process listing" do |arg| - c.tag arg + user_config.tag arg end o.on "--redirect-stdout FILE", "Redirect STDOUT to a specific file" do |arg| diff --git a/lib/puma/configuration.rb b/lib/puma/configuration.rb index 9fcacb9ab5..98243e71be 100644 --- a/lib/puma/configuration.rb +++ b/lib/puma/configuration.rb @@ -13,152 +13,147 @@ module ConfigDefault DefaultWorkerShutdownTimeout = 30 end - class LeveledOptions - def initialize(default_options, user_options) - @cur = user_options - @set = [@cur] - @defaults = default_options.dup - end - - def initialize_copy(other) - @set = @set.map { |o| o.dup } - @cur = @set.last - end - - def shift - @cur = {} - @set << @cur - end - - def reverse_shift - @cur = {} - @set.unshift(@cur) - end + # A class used for storing "leveled" configuration options. + # + # In this class any "user" specified options take precedence over any + # "file" specified options, take precedence over any "default" options. + # + # User input is prefered over "defaults": + # user_options = { foo: "bar" } + # default_options = { foo: "zoo" } + # options = UserFileDefaultOptions.new(user_options, default_options) + # puts options[:foo] + # # => "bar" + # + # All values can be accessed via `all_of` + # + # puts options.all_of(:foo) + # # => ["bar", "zoo"] + # + # A "file" option can be set. This config will be prefered over "default" options + # but will defer to any available "user" specified options. + # + # user_options = { foo: "bar" } + # default_options = { rackup: "zoo.rb" } + # options = UserFileDefaultOptions.new(user_options, default_options) + # options.file_options[:rackup] = "sup.rb" + # puts options[:rackup] + # # => "sup.rb" + # + # The "default" options can be set via procs. These are resolved during runtime + # via calls to `finalize_values` + class UserFileDefaultOptions + def initialize(user_options, default_options) + @user_options = user_options + @file_options = {} + @default_options = default_options + end + + attr_reader :user_options, :file_options, :default_options def [](key) - @set.reverse_each do |o| - if o.key? key - return o[key] - end - end - - v = @defaults[key] - if v.respond_to? :call - v.call - else - v - end + return user_options[key] if user_options.key?(key) + return file_options[key] if file_options.key?(key) + return default_options[key] if default_options.key?(key) end - def fetch(key, default=nil) - val = self[key] - return val if val - default + def []=(key, value) + user_options[key] = value end - attr_reader :cur - - def all_of(key) - all = [] - - @set.each do |o| - if v = o[key] - if v.kind_of? Array - all += v - else - all << v - end - end - end - - all - end - - def []=(key, val) - @cur[key] = val + def fetch(key, default_value = nil) + self[key] || default_value end - def key?(key) - @set.each do |o| - if o.key? key - return true - end - end - - @default.key? key - end - - def merge!(o) - o.each do |k,v| - @cur[k]= v - end - end - - def flatten - options = {} - - @set.each do |o| - o.each do |k,v| - options[k] ||= v - end - end - - options - end + def all_of(key) + user = user_options[key] + file = file_options[key] + default = default_options[key] - def explain - indent = "" + user = [user] unless user.is_a?(Array) + file = [file] unless file.is_a?(Array) + default = [default] unless default.is_a?(Array) - @set.each do |o| - o.keys.sort.each do |k| - puts "#{indent}#{k}: #{o[k].inspect}" - end + user.compact! + file.compact! + default.compact! - indent = " #{indent}" - end + user + file + default end - def force_defaults - @defaults.each do |k,v| + def finalize_values + @default_options.each do |k,v| if v.respond_to? :call - @defaults[k] = v.call + @default_options[k] = v.call end end end end + # The main configuration class of Puma. + # + # It can be initialized with a set of "user" options and "default" options. + # Defaults will be merged with `Configuration.puma_default_options`. + # + # This class works together with 2 main other classes the `UserFileDefaultOptions` + # which stores configuration options in order so the precedence is that user + # set configuration wins over "file" based configuration wins over "default" + # configuration. These configurations are set via the `DSL` class. This + # class powers the Puma config file syntax and does double duty as a configuration + # DSL used by the `Puma::CLI` and Puma rack handler. + # + # It also handles loading plugins. + # + # > Note: `:port` and `:host` are not valid keys. By they time they make it to the + # configuration options they are expected to be incorporated into a `:binds` key. + # Under the hood the DSL maps `port` and `host` calls to `:binds` + # + # config = Configuration.new({}) do |user_config, file_config, default_config| + # user_config.port 3003 + # end + # config.load + # puts config.options[:port] + # # => 3003 + # + # It is expected that `load` is called on the configuration instance after setting + # config. This method expands any values in `config_file` and puts them into the + # correct configuration option hash. + # + # Once all configuration is complete it is expected that `clamp` will be called + # on the instance. This will expand any procs stored under "default" values. This + # is done because an environment variable may have been modified while loading + # configuration files. class Configuration include ConfigDefault - def self.from_file(path) - cfg = new - - DSL.new(cfg.options, cfg)._load_from path + def initialize(user_options={}, default_options = {}, &block) + default_options = self.puma_default_options.merge(default_options) - return cfg - end - - def initialize(options={}, &blk) - @options = LeveledOptions.new(default_options, options) - - @plugins = PluginLoader.new + @options = UserFileDefaultOptions.new(user_options, default_options) + @plugins = PluginLoader.new + @user_dsl = DSL.new(@options.user_options, self) + @file_dsl = DSL.new(@options.file_options, self) + @default_dsl = DSL.new(@options.default_options, self) - if blk - configure(&blk) + if block + configure(&block) end end attr_reader :options, :plugins - def configure(&blk) - @options.shift - DSL.new(@options, self)._run(&blk) + def configure + yield @user_dsl, @file_dsl, @default_dsl + ensure + @user_dsl._offer_plugins + @file_dsl._offer_plugins + @default_dsl._offer_plugins end def initialize_copy(other) - @conf = nil + @conf = nil @cli_options = nil - @options = @options.dup + @options = @options.dup end def flatten @@ -170,7 +165,7 @@ def flatten! self end - def default_options + def puma_default_options { :min_threads => 0, :max_threads => 16, @@ -185,7 +180,7 @@ def default_options :worker_shutdown_timeout => DefaultWorkerShutdownTimeout, :remote_address => :socket, :tag => method(:infer_tag), - :environment => lambda { ENV['RACK_ENV'] || "development" }, + :environment => ->{ ENV['RACK_ENV'] || "development" }, :rackup => DefaultRackup, :logger => STDOUT, :persistent_timeout => Const::PERSISTENT_TIMEOUT @@ -206,18 +201,15 @@ def load end files.each do |f| - @options.reverse_shift - - DSL.load @options, self, f + @file_dsl._load_from(f) end - @options.shift + @options end # Call once all configuration (included from rackup files) # is loaded to flesh out any defaults def clamp - @options.shift - @options.force_defaults + @options.finalize_values end # Injects the Configuration object into the env @@ -318,17 +310,15 @@ def rack_builder def load_rackup raise "Missing rackup file '#{rackup}'" unless File.exist?(rackup) - @options.shift - rack_app, rack_options = rack_builder.parse_file(rackup) - @options.merge!(rack_options) + @options.file_options.merge!(rack_options) config_ru_binds = [] rack_options.each do |k, v| config_ru_binds << v if k.to_s.start_with?("bind") end - @options[:binds] = config_ru_binds unless config_ru_binds.empty? + @options.file_options[:binds] = config_ru_binds unless config_ru_binds.empty? rack_app end diff --git a/lib/puma/convenient.rb b/lib/puma/convenient.rb index 4ee6694635..0793c11e7c 100644 --- a/lib/puma/convenient.rb +++ b/lib/puma/convenient.rb @@ -3,12 +3,12 @@ module Puma def self.run(opts={}) - cfg = Puma::Configuration.new do |c| + cfg = Puma::Configuration.new do |user_config| if port = opts[:port] - c.port port + user_config.port port end - c.quiet + user_config.quiet yield c end diff --git a/lib/puma/dsl.rb b/lib/puma/dsl.rb index 576a7ac9ec..51b1722c9c 100644 --- a/lib/puma/dsl.rb +++ b/lib/puma/dsl.rb @@ -1,20 +1,35 @@ module Puma # The methods that are available for use inside the config file. + # These same methods are used in Puma cli and the rack handler + # internally. # + # Used manually (via CLI class): + # + # config = Configuration.new({}) do |user_config| + # user_config.port 3001 + # end + # config.load + # + # puts config.options[:binds] + # "tcp://127.0.0.1:3001" + # + # Used to load file: + # + # $ cat puma_config.rb + # port 3002 + # + # config = Configuration.new(config_file: "puma_config.rb") + # config.load + # + # puts config.options[:binds] + # # => "tcp://127.0.0.1:3002" + # + # Detailed docs can be found in `examples/config.rb` class DSL include ConfigDefault - def self.load(options, cfg, path) - d = new(options, cfg) - d._load_from(path) - - options - ensure - d._offer_plugins - end - def initialize(options, config) - @config = config + @config = config @options = options @plugins = [] @@ -40,36 +55,10 @@ def _offer_plugins @plugins.clear end - def _run(&blk) - blk.call self - ensure - _offer_plugins - end - def inject(&blk) instance_eval(&blk) end - # Load configuration from another named file. If the file name is absolute, - # load the file as an absolute path. Otherwise load it relative to the - # current config file. - # - def import(file) - if File.extname(file) == "" - file += ".rb" - end - - if file[0,1] == "/" - path = file - elsif @path - path = File.join File.dirname(@path), file - else - raise "No original configuration path to import relative to" - end - - DSL.new(@options, @config)._load_from(path) - end - def get(key,default=nil) @options[key.to_sym] || default end @@ -115,15 +104,18 @@ def activate_control_app(url="auto", opts={}) end # Load additional configuration from a file + # Files get loaded later via Configuration#load def load(file) - _ary(:config_files) << file + @options[:config_files] ||= [] + @options[:config_files] << file end # Bind the server to +url+. tcp:// and unix:// are the only accepted # protocols. # def bind(url) - _ary(:binds) << url + @options[:binds] ||= [] + @options[:binds] << url end # Define the TCP port to bind to. Use +bind+ for more advanced options. @@ -192,7 +184,8 @@ def force_shutdown_after(val=:forever) # This can be called multiple times to add code each time. # def on_restart(&block) - _ary(:on_restart) << block + @options[:on_restart] ||= [] + @options[:on_restart] << block end # Command to use to restart puma. This should be just how to @@ -297,7 +290,8 @@ def workers(count) # This can be called multiple times to add hooks. # def before_fork(&block) - _ary(:before_fork) << block + @options[:before_fork] ||= [] + @options[:before_fork] << block end # *Cluster mode only* Code to run in a worker when it boots to setup @@ -306,7 +300,8 @@ def before_fork(&block) # This can be called multiple times to add hooks. # def on_worker_boot(&block) - _ary(:before_worker_boot) << block + @options[:before_worker_boot] ||= [] + @options[:before_worker_boot] << block end # *Cluster mode only* Code to run immediately before a worker shuts @@ -317,7 +312,8 @@ def on_worker_boot(&block) # This can be called multiple times to add hooks. # def on_worker_shutdown(&block) - _ary(:before_worker_shutdown) << block + @options[:before_worker_shutdown] ||= [] + @options[:before_worker_shutdown] << block end # *Cluster mode only* Code to run in the master when it is @@ -326,7 +322,8 @@ def on_worker_shutdown(&block) # This can be called multiple times to add hooks. # def on_worker_fork(&block) - _ary(:before_worker_fork) << block + @options[:before_worker_fork] ||= [] + @options[:before_worker_fork] << block end # *Cluster mode only* Code to run in the master after it starts @@ -335,7 +332,8 @@ def on_worker_fork(&block) # This can be called multiple times to add hooks. # def after_worker_fork(&block) - _ary(:after_worker_fork) << block + @options[:after_worker_fork] ||= [] + @options[:after_worker_fork] = block end alias_method :after_worker_boot, :after_worker_fork @@ -477,10 +475,5 @@ def set_remote_address(val=:socket) end end - private - - def _ary(key) - (@options.cur[key] ||= []) - end end end diff --git a/lib/puma/launcher.rb b/lib/puma/launcher.rb index b86cc3907a..757f3eff26 100644 --- a/lib/puma/launcher.rb +++ b/lib/puma/launcher.rb @@ -34,9 +34,9 @@ class Launcher # # Examples: # - # conf = Puma::Configuration.new do |c| - # c.threads 1, 10 - # c.app do |env| + # conf = Puma::Configuration.new do |user_config| + # user_config.threads 1, 10 + # user_config.app do |env| # [200, {}, ["hello world"]] # end # end @@ -59,6 +59,7 @@ def initialize(conf, launcher_args={}) @config.load @options = @config.options + @config.clamp generate_restart_data diff --git a/lib/rack/handler/puma.rb b/lib/rack/handler/puma.rb index 8c12649c33..c1b4052f28 100644 --- a/lib/rack/handler/puma.rb +++ b/lib/rack/handler/puma.rb @@ -8,46 +8,52 @@ module Puma :Silent => false } - def self.run(app, options = {}) + def self.config(app, options = {}) require 'puma/configuration' require 'puma/events' require 'puma/launcher' - options = DEFAULT_OPTIONS.merge(options) + default_options = DEFAULT_OPTIONS.dup - conf = ::Puma::Configuration.new(options) do |c| - c.quiet + # Libraries pass in values such as :Port and there is no way to determine + # if it is a default provided by the library or a special value provided + # by the user. A special key `user_supplied_options` can be passed. This + # contains an array of all explicitly defined user options. We then + # know that all other values are defaults + if user_supplied_options = options.delete(:user_supplied_options) + (options.keys - user_supplied_options).each do |k, v| + default_options[k] = options.delete(k) + end + end + + conf = ::Puma::Configuration.new(options, default_options) do |user_config, file_config, default_config| + user_config.quiet if options.delete(:Verbose) app = Rack::CommonLogger.new(app, STDOUT) end if options[:environment] - c.environment options[:environment] + user_config.environment options[:environment] end if options[:Threads] min, max = options.delete(:Threads).split(':', 2) - c.threads min, max + user_config.threads min, max end - host = options[:Host] + self.set_host_port_to_config(options[:Host], options[:Port], user_config) + self.set_host_port_to_config(default_options[:Host], default_options[:Port], default_config) - if host && (host[0,1] == '.' || host[0,1] == '/') - c.bind "unix://#{host}" - elsif host && host =~ /^ssl:\/\// - uri = URI.parse(host) - uri.port ||= options[:Port] || ::Puma::Configuration::DefaultTCPPort - c.bind uri.to_s - else - host ||= ::Puma::Configuration::DefaultTCPHost - port = options[:Port] || ::Puma::Configuration::DefaultTCPPort + user_config.app app + end + conf + end - c.port port, host - end - c.app app - end + + def self.run(app, options = {}) + conf = self.config(app, options) events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio @@ -71,6 +77,26 @@ def self.valid_options "Verbose" => "Don't report each request (default: false)" } end + private + def self.set_host_port_to_config(host, port, config) + if host && (host[0,1] == '.' || host[0,1] == '/') + config.bind "unix://#{host}" + elsif host && host =~ /^ssl:\/\// + uri = URI.parse(host) + uri.port ||= port || ::Puma::Configuration::DefaultTCPPort + config.bind uri.to_s + else + + if host + port ||= ::Puma::Configuration::DefaultTCPPort + end + + if port + host ||= ::Puma::Configuration::DefaultTCPHost + config.port port, host + end + end + end end register :puma, Puma diff --git a/test/test_config.rb b/test/test_config.rb index 752f86acf4..2f450c1ce0 100644 --- a/test/test_config.rb +++ b/test/test_config.rb @@ -28,13 +28,12 @@ def test_app_from_app_DSL def test_double_bind_port port = (rand(10_000) + 30_000).to_s with_env("PORT" => port) do - conf = Puma::Configuration.new do |c| - c.bind "tcp://#{Puma::Configuration::DefaultTCPHost}:#{port}" - c.load "test/config/app.rb" + conf = Puma::Configuration.new do |user_config, file_config, default_config| + user_config.bind "tcp://#{Puma::Configuration::DefaultTCPHost}:#{port}" + file_config.load "test/config/app.rb" end conf.load - assert_equal ["tcp://0.0.0.0:#{port}"], conf.options[:binds] end end @@ -67,6 +66,13 @@ def test_overwrite_options assert_equal conf.options[:workers], 4 end + def test_explicit_config_files + conf = Puma::Configuration.new(config_files: ['test/config/settings.rb']) do |c| + end + conf.load + assert_match(/:3000$/, conf.options[:binds].first) + end + def test_parameters_overwrite_files conf = Puma::Configuration.new(config_files: ['test/config/settings.rb']) do |c| c.port 3030 diff --git a/test/test_helper.rb b/test/test_helper.rb index 67bf51229e..f8dd6219a8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,6 +13,8 @@ require "puma" require "puma/detect" +$LOAD_PATH << File.expand_path("../../lib", __FILE__) + # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.) def hit(uris) diff --git a/test/test_rack_handler.rb b/test/test_rack_handler.rb index dd51111dc3..88ef19c0ff 100644 --- a/test/test_rack_handler.rb +++ b/test/test_rack_handler.rb @@ -53,5 +53,111 @@ def test_handler_boots assert_equal("/test", @input["PATH_INFO"]) end end +end + +class TestUserSuppliedOptionsPortIsSet < Minitest::Test + def setup + @options = {} + @options[:user_supplied_options] = [:Port] + end + + def test_port_wins_over_config + user_port = 5001 + file_port = 6001 + + Dir.mktmpdir do |d| + Dir.chdir(d) do + FileUtils.mkdir("config") + File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } + + @options[:Port] = user_port + conf = Rack::Handler::Puma.config(->{}, @options) + conf.load + + assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] + end + end + end +end + +class TestUserSuppliedOptionsIsEmpty < Minitest::Test + def setup + @options = {} + @options[:user_supplied_options] = [] + end + + def test_config_file_wins_over_port + user_port = 5001 + file_port = 6001 + + Dir.mktmpdir do |d| + Dir.chdir(d) do + FileUtils.mkdir("config") + File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } + + @options[:Port] = user_port + conf = Rack::Handler::Puma.config(->{}, @options) + conf.load + + assert_equal ["tcp://0.0.0.0:#{file_port}"], conf.options[:binds] + end + end + end +end +class TestUserSuppliedOptionsIsNotPresent < Minitest::Test + def setup + @options = {} + end + + def test_default_port_when_no_config_file + conf = Rack::Handler::Puma.config(->{}, @options) + conf.load + + assert_equal ["tcp://0.0.0.0:9292"], conf.options[:binds] + end + + def test_config_wins_over_default + file_port = 6001 + + Dir.mktmpdir do |d| + Dir.chdir(d) do + FileUtils.mkdir("config") + File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } + + conf = Rack::Handler::Puma.config(-> {}, @options) + conf.load + + assert_equal ["tcp://0.0.0.0:#{file_port}"], conf.options[:binds] + end + end + end + + def test_user_port_wins_over_default + user_port = 5001 + @options[:Port] = user_port + conf = Rack::Handler::Puma.config(->{}, @options) + conf.load + + assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] + end + + def test_user_port_wins_over_config + user_port = 5001 + file_port = 6001 + + Dir.mktmpdir do |d| + Dir.chdir(d) do + FileUtils.mkdir("config") + File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } + + @options[:Port] = user_port + conf = Rack::Handler::Puma.config(->{}, @options) + conf.load + + assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] + end + end + end end +