diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index e3c5f887..4acee9bc 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -1,17 +1,13 @@ require 'optparse' +require 'uri' + require 'puma/configurator' require 'puma/const' module Puma class CLI - Options = [ - ['-p', '--port PORT', "Which port to bind to", :@port, 3000], - ['-a', '--address ADDR', "Address to bind to", :@address, "0.0.0.0"], - ['-n', '--concurrency INT', "Number of concurrent threads to use", - :@concurrency, 16], - ] - - Banner = "puma " + DefaultTCPHost = "0.0.0.0" + DefaultTCPPort = 3000 def initialize(argv, stdout=STDOUT) @argv = argv @@ -21,26 +17,37 @@ module Puma end def setup_options - @options = OptionParser.new do |o| - Options.each do |short, long, help, variable, default| - instance_variable_set(variable, default) + @options = { + :concurrency => 16 + } - o.on(short, long, help) do |arg| - instance_variable_set(variable, arg) - end + @binds = [] + + @parser = OptionParser.new do |o| + o.on '-n', '--concurrency INT', "Number of concurrent threads to use" do |arg| + @options[:concurrency] = arg.to_i + end + + o.on "-b", "--bind URI", "URI to bind to (tcp:// and unix:// only)" do |arg| + @binds << arg end end - @options.banner = Banner + @parser.banner = "puma " - @options.on_tail "-h", "--help", "Show help" do - @stdout.puts @options + @parser.on_tail "-h", "--help", "Show help" do + @stdout.puts @parser exit 1 end end + def load_rackup + @app, options = Rack::Builder.parse_file @rackup + @options.merge! options + end + def run - @options.parse! @argv + @parser.parse! @argv @rackup = ARGV.shift || "config.ru" @@ -48,24 +55,46 @@ module Puma raise "Missing rackup file '#{@rackup}'" end - settings = { - :host => @address, - :port => @port, - :concurrency => @concurrency, - :stdout => @stdout - } + load_rackup - config = Puma::Configurator.new(settings) do |c| - c.listener do |l| - l.load_rackup @rackup + if @binds.empty? + @options[:Host] ||= DefaultTCPHost + @options[:Port] ||= DefaultTCPPort + end + + server = Puma::Server.new @app, @options[:concurrency] + + @stdout.puts "Puma #{Puma::Const::PUMA_VERSION} starting..." + + if @options[:Host] + @stdout.puts "Listening on tcp://#{@options[:Host]}:#{@options[:Port]}" + server.add_tcp_listener @options[:Host], @options[:Port] + end + + @binds.each do |str| + uri = URI.parse str + case uri.scheme + when "tcp" + @stdout.puts "Listening on #{str}" + server.add_tcp_listener uri.host, uri.port + when "unix" + @stdout.puts "Listening on #{str}" + if uri.host + path = "#{uri.host}/#{uri.path}" + else + path = uri.path + end + + server.add_unix_listener path + else + @stdout.puts "Invalid URI: #{str}" + exit 1 end end - config.run - config.log "Puma #{Puma::Const::PUMA_VERSION} available at #{@address}:#{@port}" - config.log "Use CTRL-C to stop." + @stdout.puts "Use Ctrl-C to stop" - config.join + server.run.join end end end diff --git a/lib/puma/configurator.rb b/lib/puma/configurator.rb index 68c67832..278d30c1 100644 --- a/lib/puma/configurator.rb +++ b/lib/puma/configurator.rb @@ -1,4 +1,3 @@ -require 'yaml' require 'etc' require 'rubygems' @@ -8,9 +7,7 @@ require 'puma/server' 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. + # purposes. # # It is used like this: # @@ -40,15 +37,13 @@ module Puma 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 = {} + @listeners = [] @defaults = defaults - @needs_restart = false if block yield self @@ -98,7 +93,7 @@ module Puma @listener = Puma::Server.new(ops[:host], ops[:port].to_i, ops[:concurrency].to_i) @listener_name = "#{ops[:host]}:#{ops[:port]}" - @listeners[@listener_name] = @listener + @listeners << @listener if ops[:user] and ops[:group] change_privilege(ops[:user], ops[:group]) @@ -118,28 +113,20 @@ module Puma # Do something with options? end - # Easy way to load a YAML file and apply default settings. - def load_yaml(file, default={}) - default.merge(YAML.load_file(file)) - 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 - } + @listeners.each { |s| s.run } 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| + def stop(synchronous=false) + @listeners.each do |s| s.stop(synchronous) end - @needs_restart = needs_restart end @@ -147,7 +134,7 @@ module Puma # 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 } + @listeners.each { |s| s.acceptor.join } end # Used to allow you to let users specify their own configurations diff --git a/lib/puma/server.rb b/lib/puma/server.rb index fbfe8d11..0d972d00 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -1,4 +1,5 @@ require 'rack' +require 'stringio' require 'puma/thread_pool' require 'puma/const' @@ -21,8 +22,6 @@ module Puma attr_accessor :stderr, :stdout - Default = lambda { |e| raise "no rack app configured" } - # Creates a working server on host:port (strange things happen if port # isn't a Number). # @@ -33,14 +32,13 @@ module Puma # the same time. Any requests over this ammount are queued and handled # as soon as a thread is available. # - def initialize(host, port, concurrent=10, app=Default) - @socket = TCPServer.new(host, port) - - @host = host - @port = port + def initialize(app, concurrent=10) @concurrent = concurrent @check, @notify = IO.pipe + + @ios = [@check] + @running = true @thread_pool = ThreadPool.new(0, concurrent) do |client| @@ -53,24 +51,24 @@ module Puma @app = app end + def add_tcp_listener(host, port) + @ios << TCPServer.new(host, port) + end + + def add_unix_listener(path) + @ios << UNIXServer.new(path) + end + # Runs the server. It returns the thread used so you can "join" it. # You can also access the HttpServer#acceptor attribute to get the # thread later. def run BasicSocket.do_not_reverse_lookup = true - configure_socket_options - - if @tcp_defer_accept_opts - @socket.setsockopt(*@tcp_defer_accept_opts) - end - - tcp_cork_opts = @tcp_cork_opts - @acceptor = Thread.new do begin check = @check - sockets = [check, @socket] + sockets = @ios pool = @thread_pool while @running @@ -80,11 +78,7 @@ module Puma if sock == check break if handle_check else - client = sock.accept - - client.setsockopt(*tcp_cork_opts) if tcp_cork_opts - - pool << client + pool << sock.accept end end rescue Errno::ECONNABORTED @@ -97,35 +91,13 @@ module Puma end graceful_shutdown ensure - @socket.close - # @stderr.puts "#{Time.now}: Closed socket." + @ios.each { |i| i.close } end end return @acceptor end - def configure_socket_options - @tcp_defer_accept_opts = nil - @tcp_cork_opts = nil - - case RUBY_PLATFORM - when /linux/ - # 9 is currently TCP_DEFER_ACCEPT - @tcp_defer_accept_opts = [Socket::SOL_TCP, 9, 1] - @tcp_cork_opts = [Socket::SOL_TCP, 3, 1] - - when /freebsd(([1-4]\..{1,2})|5\.[0-4])/ - # Do nothing, just closing a bug when freebsd <= 5.4 - when /freebsd/ - # Use the HTTP accept filter if available. - # The struct made by pack() is defined in /usr/include/sys/socket.h as accept_filter_arg - unless `/sbin/sysctl -nq net.inet.accf.http`.empty? - @tcp_defer_accept_opts = [Socket::SOL_SOCKET, Socket::SO_ACCEPTFILTER, ['httpready', nil].pack('a16a240')] - end - end - end - def handle_check cmd = @check.read(1) @@ -351,6 +323,4 @@ module Puma @acceptor.join if sync end end - - HttpServer = Server end diff --git a/test/test_rack_server.rb b/test/test_rack_server.rb index 5a0c0fdc..5ee550fa 100644 --- a/test/test_rack_server.rb +++ b/test/test_rack_server.rb @@ -42,9 +42,9 @@ class TestRackServer < Test::Unit::TestCase def setup @valid_request = "GET / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\n\r\n" - @server = Puma::Server.new("127.0.0.1", 9998) @simple = lambda { |env| [200, { "X-Header" => "Works" }, "Hello"] } - @server.app = @simple + @server = Puma::Server.new @simple + @server.add_tcp_listener "127.0.0.1", 9998 end def teardown diff --git a/test/test_unix_socket.rb b/test/test_unix_socket.rb new file mode 100644 index 00000000..b79d6837 --- /dev/null +++ b/test/test_unix_socket.rb @@ -0,0 +1,32 @@ +require 'test/unit' +require 'puma/server' + +require 'socket' + +class TestPumaUnixSocket < Test::Unit::TestCase + + App = lambda { |env| [200, {}, ["Works"]] } + + Path = "test/puma.sock" + + def setup + @server = Puma::Server.new App, 2 + end + + def teardown + @server.stop(true) + File.unlink Path if File.exists? Path + end + + def test_server + @server.add_unix_listener Path + @server.run + + sock = UNIXSocket.new Path + + sock << "GET / HTTP/1.0\r\nHost: blah.com\r\n\r\n" + + assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nWorks", + sock.read + end +end diff --git a/test/test_ws.rb b/test/test_ws.rb index e46f6d1d..8f981bb7 100644 --- a/test/test_ws.rb +++ b/test/test_ws.rb @@ -20,11 +20,13 @@ class WebServerTest < Test::Unit::TestCase def setup @valid_request = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n" - @server = HttpServer.new("127.0.0.1", 9998) - @server.stderr = StringIO.new - @tester = TestHandler.new + @server = Server.new @tester + @server.add_tcp_listener "127.0.0.1", 9998 + + @server.stderr = StringIO.new + @server.app = @tester redirect_test_io do