lib/webrick/server.rb in webrick-1.3.1 vs lib/webrick/server.rb in webrick-1.4.0.beta1
- old
+ new
@@ -1,5 +1,6 @@
+# frozen_string_literal: false
#
# server.rb -- GenericServer Class
#
# Author: IPR -- Internet Programming with Ruby -- writers
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
@@ -13,13 +14,23 @@
require 'webrick/config'
require 'webrick/log'
module WEBrick
+ ##
+ # Server error exception
+
class ServerError < StandardError; end
+ ##
+ # Base server class
+
class SimpleServer
+
+ ##
+ # A SimpleServer only yields when you start it
+
def SimpleServer.start
yield
end
end
@@ -31,40 +42,68 @@
##
# Performs the standard operations for daemonizing a process. Runs a
# block, if given.
def Daemon.start
- exit!(0) if fork
- Process::setsid
- exit!(0) if fork
- Dir::chdir("/")
- File::umask(0)
- STDIN.reopen("/dev/null")
- STDOUT.reopen("/dev/null", "w")
- STDERR.reopen("/dev/null", "w")
+ Process.daemon
+ File.umask(0)
yield if block_given?
end
end
+ ##
+ # Base TCP server class. You must subclass GenericServer and provide a #run
+ # method.
+
class GenericServer
- attr_reader :status, :config, :logger, :tokens, :listeners
+ ##
+ # The server status. One of :Stop, :Running or :Shutdown
+
+ attr_reader :status
+
+ ##
+ # The server configuration
+
+ attr_reader :config
+
+ ##
+ # The server logger. This is independent from the HTTP access log.
+
+ attr_reader :logger
+
+ ##
+ # Tokens control the number of outstanding clients. The
+ # <code>:MaxClients</code> configuration sets this.
+
+ attr_reader :tokens
+
+ ##
+ # Sockets listening for connections.
+
+ attr_reader :listeners
+
+ ##
+ # Creates a new generic server from +config+. The default configuration
+ # comes from +default+.
+
def initialize(config={}, default=Config::General)
@config = default.dup.update(config)
@status = :Stop
@config[:Logger] ||= Log::new
@logger = @config[:Logger]
- @tokens = SizedQueue.new(@config[:MaxClients])
+ @tokens = Thread::SizedQueue.new(@config[:MaxClients])
@config[:MaxClients].times{ @tokens.push(nil) }
webrickv = WEBrick::VERSION
rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
@logger.info("WEBrick #{webrickv}")
@logger.info("ruby #{rubyv}")
@listeners = []
+ @shutdown_pipe = nil
unless @config[:DoNotListen]
if @config[:Listen]
warn(":Listen option is deprecated; use GenericServer#listen")
end
listen(@config[:BindAddress], @config[:Port])
@@ -72,112 +111,172 @@
@config[:Port] = @listeners[0].addr[1]
end
end
end
+ ##
+ # Retrieves +key+ from the configuration
+
def [](key)
@config[key]
end
+ ##
+ # Adds listeners from +address+ and +port+ to the server. See
+ # WEBrick::Utils::create_listeners for details.
+
def listen(address, port)
- @listeners += Utils::create_listeners(address, port, @logger)
+ @listeners += Utils::create_listeners(address, port)
end
+ ##
+ # Starts the server and runs the +block+ for each connection. This method
+ # does not return until the server is stopped from a signal handler or
+ # another thread using #stop or #shutdown.
+ #
+ # If the block raises a subclass of StandardError the exception is logged
+ # and ignored. If an IOError or Errno::EBADF exception is raised the
+ # exception is ignored. If an Exception subclass is raised the exception
+ # is logged and re-raised which stops the server.
+ #
+ # To completely shut down a server call #shutdown from ensure:
+ #
+ # server = WEBrick::GenericServer.new
+ # # or WEBrick::HTTPServer.new
+ #
+ # begin
+ # server.start
+ # ensure
+ # server.shutdown
+ # end
+
def start(&block)
raise ServerError, "already started." if @status != :Stop
server_type = @config[:ServerType] || SimpleServer
+ setup_shutdown_pipe
+
server_type.start{
@logger.info \
"#{self.class}#start: pid=#{$$} port=#{@config[:Port]}"
call_callback(:StartCallback)
+ shutdown_pipe = @shutdown_pipe
+
thgroup = ThreadGroup.new
@status = :Running
- while @status == :Running
- begin
- if svrs = IO.select(@listeners, nil, nil, 2.0)
- svrs[0].each{|svr|
- @tokens.pop # blocks while no token is there.
- if sock = accept_client(svr)
- sock.do_not_reverse_lookup = config[:DoNotReverseLookup]
- th = start_thread(sock, &block)
- th[:WEBrickThread] = true
- thgroup.add(th)
- else
- @tokens.push(nil)
+ begin
+ while @status == :Running
+ begin
+ sp = shutdown_pipe[0]
+ if svrs = IO.select([sp, *@listeners], nil, nil, 2.0)
+ if svrs[0].include? sp
+ # swallow shutdown pipe
+ buf = String.new
+ nil while String ===
+ sp.read_nonblock([sp.nread, 8].max, buf, exception: false)
+ break
end
- }
+ svrs[0].each{|svr|
+ @tokens.pop # blocks while no token is there.
+ if sock = accept_client(svr)
+ unless config[:DoNotReverseLookup].nil?
+ sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup]
+ end
+ th = start_thread(sock, &block)
+ th[:WEBrickThread] = true
+ thgroup.add(th)
+ else
+ @tokens.push(nil)
+ end
+ }
+ end
+ rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex
+ # if the listening socket was closed in GenericServer#shutdown,
+ # IO::select raise it.
+ rescue StandardError => ex
+ msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
+ @logger.error msg
+ rescue Exception => ex
+ @logger.fatal ex
+ raise
end
- rescue Errno::EBADF, IOError => ex
- # if the listening socket was closed in GenericServer#shutdown,
- # IO::select raise it.
- rescue Exception => ex
- msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
- @logger.error msg
end
+ ensure
+ cleanup_shutdown_pipe(shutdown_pipe)
+ cleanup_listener
+ @status = :Shutdown
+ @logger.info "going to shutdown ..."
+ thgroup.list.each{|th| th.join if th[:WEBrickThread] }
+ call_callback(:StopCallback)
+ @logger.info "#{self.class}#start done."
+ @status = :Stop
end
-
- @logger.info "going to shutdown ..."
- thgroup.list.each{|th| th.join if th[:WEBrickThread] }
- call_callback(:StopCallback)
- @logger.info "#{self.class}#start done."
- @status = :Stop
}
end
+ ##
+ # Stops the server from accepting new connections.
+
def stop
if @status == :Running
@status = :Shutdown
end
+
+ alarm_shutdown_pipe {|f| f.write_nonblock("\0")}
end
+ ##
+ # Shuts down the server and all listening sockets. New listeners must be
+ # provided to restart the server.
+
def shutdown
stop
- @listeners.each{|s|
- if @logger.debug?
- addr = s.addr
- @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})")
- end
- begin
- s.shutdown
- rescue Errno::ENOTCONN
- # when `Errno::ENOTCONN: Socket is not connected' on some platforms,
- # call #close instead of #shutdown.
- # (ignore @config[:ShutdownSocketWithoutClose])
- s.close
- else
- unless @config[:ShutdownSocketWithoutClose]
- s.close
- end
- end
- }
- @listeners.clear
+
+ alarm_shutdown_pipe(&:close)
end
+ ##
+ # You must subclass GenericServer and implement \#run which accepts a TCP
+ # client socket
+
def run(sock)
@logger.fatal "run() must be provided by user."
end
private
+ # :stopdoc:
+
+ ##
+ # Accepts a TCP client socket from the TCP server socket +svr+ and returns
+ # the client socket.
+
def accept_client(svr)
sock = nil
begin
sock = svr.accept
sock.sync = true
Utils::set_non_blocking(sock)
- Utils::set_close_on_exec(sock)
rescue Errno::ECONNRESET, Errno::ECONNABORTED,
- Errno::EPROTO, Errno::EINVAL => ex
- rescue Exception => ex
+ Errno::EPROTO, Errno::EINVAL
+ rescue StandardError => ex
msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
@logger.error msg
end
return sock
end
+ ##
+ # Starts a server thread for the client socket +sock+ that runs the given
+ # +block+.
+ #
+ # Sets the socket to the <code>:WEBrickSocket</code> thread local variable
+ # in the thread.
+ #
+ # If any errors occur in the block they are logged and handled.
+
def start_thread(sock, &block)
Thread.start{
begin
Thread.current[:WEBrickSocket] = sock
begin
@@ -207,12 +306,56 @@
sock.close
end
}
end
+ ##
+ # Calls the callback +callback_name+ from the configuration with +args+
+
def call_callback(callback_name, *args)
- if cb = @config[callback_name]
- cb.call(*args)
+ @config[callback_name]&.call(*args)
+ end
+
+ def setup_shutdown_pipe
+ return @shutdown_pipe ||= IO.pipe
+ end
+
+ def cleanup_shutdown_pipe(shutdown_pipe)
+ @shutdown_pipe = nil
+ shutdown_pipe&.each(&:close)
+ end
+
+ def alarm_shutdown_pipe
+ _, pipe = @shutdown_pipe # another thread may modify @shutdown_pipe.
+ if pipe
+ if !pipe.closed?
+ begin
+ yield pipe
+ rescue IOError # closed by another thread.
+ end
+ end
end
+ end
+
+ def cleanup_listener
+ @listeners.each{|s|
+ if @logger.debug?
+ addr = s.addr
+ @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})")
+ end
+ begin
+ s.shutdown
+ rescue Errno::ENOTCONN
+ # when `Errno::ENOTCONN: Socket is not connected' on some platforms,
+ # call #close instead of #shutdown.
+ # (ignore @config[:ShutdownSocketWithoutClose])
+ s.close
+ else
+ unless @config[:ShutdownSocketWithoutClose]
+ s.close
+ end
+ end
+ }
+ @listeners.clear
end
end # end of GenericServer
end